// SPDX-FileCopyrightText: 2014 Ilya V. Matveychikov <i.matveychikov@milabs.ru>
// SPDX-FileCopyrightText: 2014 montekki <fedor.sakharov@gmail.com>
// SPDX-FileCopyrightText: 2026 RizinOrg <info@rizin.re>
// SPDX-License-Identifier: LGPL-3.0-only

#include <string.h>
#include <rz_types.h>
#include <rz_endian.h>
#include <rz_analysis.h>

#include "c55x_analysis.h"
#include "../tms320c55x_insn.h"
#include "../c55_ir.h"
#include "../c55x_plus/c55plus_analysis.h"

/**
 * \file c55x_analysis.c
 *
 * TMS320C55x (base) analysis: classify opcodes, resolve branch
 * targets, set basic-block fallthrough, fill in stack effects.
 *
 * Pure byte-level dispatch -- no mnemonic-string matching. Each
 * recognised opcode is dispatched on its leading byte (with second-
 * byte refinement where the prefix family is shared by multiple
 * instructions).
 *
 * The encoding map below was extracted from TI SPRU374 (TMS320C55x
 * DSP Mnemonic Instruction Set Reference Guide, public) and cross-
 * referenced against the rizin c55x decoder's internal opcode table
 * (librz/arch/isa/tms320/c55x/table.h, originally by th0rpe 2013).
 *
 * Key differences from the C55x+ ('+'-suffixed Ryujin / Low-Power
 * C55x) instruction set:
 *
 *   - 0x21 is the parallel-instruction marker (`|| nop`), NOT RET.
 *     RET in baseline C55x is encoded as the 2-byte form 0x48 0x88;
 *     RETI as 0x48 0xA8.
 *   - 0x00 is RPTCC (3-byte conditional repeat), NOT NOP_16.
 *     NOP is the 1-byte 0x20 (same as C55x+).
 *   - 0x02 is RETCC (conditional return), NOT B/CALL indirect.
 *   - 0x04 / 0x06 / 0x08 / 0x4A are the short B/BCC/CALL forms;
 *     0x6A / 0x6B / 0x6C / 0x6E are the 24-bit absolute forms.
 *   - INTR / TRAP are 2-byte 0x95 ?? (bit 7 of byte 2 selects).
 *   - Many control-flow opcodes use a second-byte high bit to flip
 *     between B / CALL or related variants.
 *
 * Encoding cross-validated against rz-asm output for the testbins
 * c55x emulateme binary (tms320/emulateme_nostd.ccsv5.c55x.ticoff2.*)
 * and against TI SPRU374 sec.4 (Instruction Set Reference).
 *
 * Byte-order note: C55x branch displacements and absolute targets
 * are stored MSB-first within the instruction stream -- see SPRU374
 * sec.3. Extracted with rz_read_at_be16() / rz_read_at_be24() to avoid
 * any unaligned-int dereference.
 */

/* Per-leading-byte instruction size, extracted from the c55x
 * decoder's opcode table (librz/arch/isa/tms320/c55x/table.h --
 * originally by th0rpe 2013, sourced from TI SPRU374). Bytes not
 * documented in the table default to size=1 so the analyzer
 * advances and re-syncs on the next byte rather than getting stuck.
 *
 * Note that some c55x instructions are even longer (up to 7 bytes
 * total) due to immediate-operand suffixes; the table value is the
 * size of the *leading* fixed-form, not necessarily the size of the
 * decoded instruction. This works for analysis purposes (we don't
 * need exact instruction boundaries; we need enough bytes to read
 * branch displacements and at least classify the type).
 *
 * For boundary-critical analyses (basic-block formation), the
 * disassembler is still authoritative; the analyzer's size estimate
 * just needs to be non-zero and not lie about a branch's
 * displacement bytes being there. */
static const ut8 c55x_op_sizes[256] = {
	3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 1, 1, 3, 3, 3, 3, /* 0x00 */
	3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, /* 0x10 */
	1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, /* 0x20 */
	2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, /* 0x30 */
	2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, /* 0x40 */
	2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, /* 0x50 */
	2, 2, 2, 2, 2, 2, 2, 2, 5, 5, 4, 4, 4, 4, 4, 4, /* 0x60 */
	4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, /* 0x70 */
	3, 3, 4, 4, 4, 4, 4, 4, 1, 1, 1, 1, 1, 1, 1, 1, /* 0x80 */
	2, 2, 2, 1, 2, 2, 2, 1, 1, 1, 1, 1, 1, 1, 2, 2, /* 0x90 */
	2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, /* 0xa0 */
	2, 1, 1, 1, 2, 2, 2, 2, 2, 1, 1, 2, 2, 1, 1, 1, /* 0xb0 */
	2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, /* 0xc0 */
	3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, /* 0xd0 */
	3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, /* 0xe0 */
	4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 1, 1, /* 0xf0 */
};

static int c55x_op_size(const ut8 *buf, int len) {
	if (len < 1) {
		return 0;
	}
	const ut8 sz = c55x_op_sizes[buf[0]];
	return ((int)sz <= len) ? (int)sz : 0;
}

/* ---- shared decode-IR descriptor (incremental C55x cutover) -----------
 * The shared c55_ir engine decodes a C55Insn once; the disassembler,
 * analyzer and RzIL lifter then consume it. This is the first member of the
 * TMS320C55x family wired onto the same engine as C55x+, validating that the
 * IR/consumers are arch-agnostic. Only opcodes present in c55x_table take
 * this path; everything else falls through to the legacy byte-driven code,
 * so the migration stays behaviour-preserving at every step.
 *
 * The instruction ids and 23-bit data-pointer model are common to the family,
 * so id->mnemonic and id->type are resolved exactly as for C55x+. The
 * descriptor carries the operand-free nop and the register-to-register mov
 * family; reg_info resolves the C55x register file (AC0-3 / T0-3 / AR0-7),
 * a subset of the C55x+ file with identical il_vars and widths. */
// C55x register file (a subset of C55x+; identical il_vars/widths): the
// register-to-register forms reach AC0-3, T0-3 and AR0-7.
static const C55RegInfo c55x_ac_ri[4] = {
	{ "ac0", "ac0", 40 }, { "ac1", "ac1", 40 }, { "ac2", "ac2", 40 }, { "ac3", "ac3", 40 }
};
static const C55RegInfo c55x_t_ri[4] = {
	{ "t0", "t0", 16 }, { "t1", "t1", 16 }, { "t2", "t2", 16 }, { "t3", "t3", 16 }
};
static const C55RegInfo c55x_ar_ri[8] = {
	{ "ar0", "ar0", 16 }, { "ar1", "ar1", 16 }, { "ar2", "ar2", 16 }, { "ar3", "ar3", 16 },
	{ "ar4", "ar4", 16 }, { "ar5", "ar5", 16 }, { "ar6", "ar6", 16 }, { "ar7", "ar7", 16 }
};
// The ARn registers are 16-bit for data, but the address pointer they form is
// the 23-bit XARn (used to compute effective addresses and apply post-modify).
static const C55RegInfo c55x_xar_ri[8] = {
	{ "xar0", "xar0", 23 }, { "xar1", "xar1", 23 }, { "xar2", "xar2", 23 }, { "xar3", "xar3", 23 },
	{ "xar4", "xar4", 23 }, { "xar5", "xar5", 23 }, { "xar6", "xar6", 23 }, { "xar7", "xar7", 23 }
};
static const C55RegInfo c55x_sp_ri = { "sp", "sp", 16 };
// The coefficient data pointer: rendered "cdp", but the address pointer it
// forms (used for effective-address and post-modify of the Cmem coefficient
// operand) is the 23-bit XCDP, so its il_var is xcdp.
static const C55RegInfo c55x_cdp_ri = { "cdp", "xcdp", 23 };
// The four C55x status registers ST0_55..ST3_55, written by the bclr / bset
// bit-clear / bit-set forms (opcode 0x46). They are 16-bit control registers
// with matching lifter variables.
static const C55RegInfo c55x_st_ri[4] = {
	{ "st0_55", "st0_55", 16 }, { "st1_55", "st1_55", 16 },
	{ "st2_55", "st2_55", 16 }, { "st3_55", "st3_55", 16 }
};
// The test-control status bits, written by the btst bit-test forms. Rendered
// TC1 / TC2 (uppercase); they carry no lifter variable here because the
// bit-test instruction that names them is not yet lifted.
static const C55RegInfo c55x_tc_ri[2] = {
	{ "TC1", NULL, 1 },
	{ "TC2", NULL, 1 }
};
// Named status flags and special registers used as explicit operands. CARRY /
// BORROW are the 0xdf carry/borrow flags; the rest are the destinations of the
// 0xdc mov Smem, <special-reg> forms. These are the 16-bit views the legacy
// lifter writes (e.g. dp / cdp, not the 23-bit xcdp), so they carry their own
// il_var here; dph is 7-bit and pdp is 9-bit, the rest 16-bit.
enum {
	C55X_SPR_CARRY = 0,
	C55X_SPR_BORROW,
	C55X_SPR_DP,
	C55X_SPR_CDP,
	C55X_SPR_BSA01,
	C55X_SPR_BSA23,
	C55X_SPR_BSA45,
	C55X_SPR_BSA67,
	C55X_SPR_BSAC,
	C55X_SPR_SP,
	C55X_SPR_SSP,
	C55X_SPR_BK03,
	C55X_SPR_BK47,
	C55X_SPR_BKC,
	C55X_SPR_DPH,
	C55X_SPR_PDP,
	C55X_SPR_CSR,
	C55X_SPR_BRC0,
	C55X_SPR_BRC1,
	C55X_SPR_TRN0,
	C55X_SPR_TRN1,
	C55X_SPR_RPTC,
	// 23-bit extended pointers named by the popboth / pshboth register-pair
	// stack ops (the "xdst"/"xsrc" field). They carry no lifter variable: the
	// pair push-pop is left unlifted.
	C55X_SPR_XSP,
	C55X_SPR_XSSP,
	C55X_SPR_XDP,
	C55X_SPR_XCDP,
	C55X_SPR_COUNT
};
static const C55RegInfo c55x_special_ri[C55X_SPR_COUNT] = {
	{ "CARRY", NULL, 1 }, { "BORROW", NULL, 1 },
	{ "dp", "dp", 16 }, { "cdp", "cdp", 16 }, { "bsa01", "bsa01", 16 },
	{ "bsa23", "bsa23", 16 }, { "bsa45", "bsa45", 16 }, { "bsa67", "bsa67", 16 },
	{ "bsac", "bsac", 16 }, { "sp", "sp", 16 }, { "ssp", "ssp", 16 },
	{ "bk03", "bk03", 16 }, { "bk47", "bk47", 16 }, { "bkc", "bkc", 16 },
	{ "dph", "dph", 7 }, { "pdp", "pdp", 9 }, { "csr", "csr", 16 },
	{ "brc0", "brc0", 16 }, { "brc1", "brc1", 16 }, { "trn0", "trn0", 16 },
	{ "trn1", "trn1", 16 }, { "rptc", "rptc", 16 },
	{ "xsp", NULL, 23 }, { "xssp", NULL, 23 }, { "xdp", NULL, 23 }, { "xcdp", NULL, 23 }
};

static const C55RegInfo *c55x_reg_info(C55RegClass cls, ut8 num, C55SubReg sub) {
	(void)sub;
	switch (cls) {
	case C55_RC_AC: return num < 4 ? &c55x_ac_ri[num] : NULL;
	case C55_RC_T: return num < 4 ? &c55x_t_ri[num] : NULL;
	case C55_RC_AR: return num < 8 ? &c55x_ar_ri[num] : NULL;
	case C55_RC_XAR: return num < 8 ? &c55x_xar_ri[num] : NULL;
	case C55_RC_SP: return num == 0 ? &c55x_sp_ri : NULL;
	case C55_RC_CDP: return num == 0 ? &c55x_cdp_ri : NULL;
	case C55_RC_ST: return num < 4 ? &c55x_st_ri[num] : NULL;
	case C55_RC_TC: return num < 2 ? &c55x_tc_ri[num] : NULL;
	case C55_RC_SPECIAL: return num < C55X_SPR_COUNT ? &c55x_special_ri[num] : NULL;
	default: return NULL;
	}
}

// C55x 4-bit register selector (TI "register field"): 0-3 -> AC0-3,
// 4-7 -> T0-3, 8-15 -> AR0-7. Shared by the register-to-register forms.
static void c55x_gr4(ut8 nib, C55Reg *r) {
	r->sub = C55_SUB_NONE;
	if (nib < 4) {
		r->cls = C55_RC_AC;
		r->num = nib;
	} else if (nib < 8) {
		r->cls = C55_RC_T;
		r->num = (ut8)(nib - 4);
	} else {
		r->cls = C55_RC_AR;
		r->num = (ut8)(nib - 8);
	}
}

// gr4 for the address-arithmetic forms (amov / asub ACx, ACy): the high range
// names the 23-bit XARn pointer rather than the 16-bit ARn.
static void c55x_gr4a(ut8 nib, C55Reg *r) {
	r->sub = C55_SUB_NONE;
	if (nib < 4) {
		r->cls = C55_RC_AC;
		r->num = nib;
	} else if (nib < 8) {
		r->cls = C55_RC_T;
		r->num = (ut8)(nib - 4);
	} else {
		r->cls = C55_RC_XAR;
		r->num = (ut8)(nib - 8);
	}
}

static void c55x_x_gr4a(const C55ArchDesc *a, ut64 bits, const C55OpDesc *d, C55Operand *out) {
	out->kind = C55_OP_REG;
	c55x_gr4a((ut8)((bits >> d->lo) & 0x0f), &out->reg);
	const C55RegInfo *ri = a->reg_info ? a->reg_info(out->reg.cls, out->reg.num, C55_SUB_NONE) : NULL;
	out->width = ri ? ri->width : 0;
}

// The fixed CSR (computed single-repeat) register operand of the rpt / rptadd /
// rptsub forms.
static void c55x_x_csr(const C55ArchDesc *a, ut64 bits, const C55OpDesc *d, C55Operand *out) {
	(void)bits;
	(void)d;
	out->kind = C55_OP_REG;
	out->reg.cls = C55_RC_SPECIAL;
	out->reg.num = C55X_SPR_CSR;
	out->reg.sub = C55_SUB_NONE;
	const C55RegInfo *ri = a->reg_info ? a->reg_info(out->reg.cls, out->reg.num, C55_SUB_NONE) : NULL;
	out->width = ri ? ri->width : 0;
}

// The transition register (trn0/trn1) operand of the dmaxdiff / dmindiff forms;
// the selecting bit is at d->lo.
static void c55x_x_trn(const C55ArchDesc *a, ut64 bits, const C55OpDesc *d, C55Operand *out) {
	out->kind = C55_OP_REG;
	out->reg.cls = C55_RC_SPECIAL;
	out->reg.num = (ut8)(C55X_SPR_TRN0 + ((bits >> d->lo) & 0x1));
	out->reg.sub = C55_SUB_NONE;
	const C55RegInfo *ri = a->reg_info ? a->reg_info(out->reg.cls, out->reg.num, C55_SUB_NONE) : NULL;
	out->width = ri ? ri->width : 0;
}

static void c55x_x_gr4(const C55ArchDesc *a, ut64 bits, const C55OpDesc *d, C55Operand *out) {
	out->kind = C55_OP_REG;
	c55x_gr4((ut8)((bits >> d->lo) & 0x0f), &out->reg);
	const C55RegInfo *ri = a->reg_info ? a->reg_info(out->reg.cls, out->reg.num, C55_SUB_NONE) : NULL;
	out->width = ri ? ri->width : 0;
}

// As c55x_x_gr4, but the operand is omitted when it names the same register as
// the immediately-preceding operand (the unary "not/neg/abs ACx" forms collapse
// the destination against the source: "not ac0" rather than "not ac0, ac0").
static void c55x_x_gr4_elide(const C55ArchDesc *a, ut64 bits, const C55OpDesc *d, C55Operand *out) {
	c55x_x_gr4(a, bits, d, out);
	out->elide_if_eq_prev = true;
}

// Accumulator rendered with the dbl(...) wrapper -- the "dbl(ACx)" operand of
// the pop / psh dbl(ACx) stack forms (opcode 0x50, sub-opcodes 3/7). The dbl
// form only ever names an accumulator: the field's low two bits pick AC0-AC3
// (the legacy decoder ignores the upper bits). The dbl marker is a disassembly
// decoration; the lifted op is the plain accumulator pop / psh.
static void c55x_x_ac_dbl(const C55ArchDesc *a, ut64 bits, const C55OpDesc *d, C55Operand *out) {
	out->kind = C55_OP_REG;
	out->reg.cls = C55_RC_AC;
	out->reg.num = (ut8)((bits >> d->lo) & 0x3);
	out->reg.sub = C55_SUB_NONE;
	out->dbl = true;
	const C55RegInfo *ri = a->reg_info ? a->reg_info(out->reg.cls, out->reg.num, C55_SUB_NONE) : NULL;
	out->width = ri ? ri->width : 0;
}

// hi(ACx) accumulator high-word source of the "mov hi(ACx), dst" forms (opcode
// 0x44, high nibble 0-3): the accumulator index is the field's low two bits;
// the operand is the 16-bit high word (rendered hi(acN)). The shared MOV lifter
// sign-extends it to the destination width.
static void c55x_x_hi_ac(const C55ArchDesc *a, ut64 bits, const C55OpDesc *d, C55Operand *out) {
	out->kind = C55_OP_REG;
	out->reg.cls = C55_RC_AC;
	out->reg.num = (ut8)((bits >> d->lo) & 0x3);
	out->reg.sub = C55_SUB_HI;
	const C55RegInfo *ri = a->reg_info ? a->reg_info(out->reg.cls, out->reg.num, C55_SUB_NONE) : NULL;
	out->width = ri ? ri->width : 0;
}

// Special-register source of the "mov <reg>, dst" forms (opcode 0x44, high
// nibble 8-15): 8 -> SP, 9 -> SSP, 10 -> CDP, 12 -> BRC0, 13 -> BRC1,
// 14 -> RPTC (the 16-bit views). 11 and 15 are unassigned -> left to the legacy
// decoder. The shared MOV lifter sign-extends the source to the dst width.
static void c55x_x_44src(const C55ArchDesc *a, ut64 bits, const C55OpDesc *d, C55Operand *out) {
	ut8 v = (ut8)((bits >> d->lo) & 0x0f);
	ut8 spr;
	switch (v) {
	case 8: spr = C55X_SPR_SP; break;
	case 9: spr = C55X_SPR_SSP; break;
	case 10: spr = C55X_SPR_CDP; break;
	case 12: spr = C55X_SPR_BRC0; break;
	case 13: spr = C55X_SPR_BRC1; break;
	case 14: spr = C55X_SPR_RPTC; break;
	default:
		out->kind = C55_OP_INVALID;
		return;
	}
	out->kind = C55_OP_REG;
	out->reg.cls = C55_RC_SPECIAL;
	out->reg.num = spr;
	out->reg.sub = C55_SUB_NONE;
	const C55RegInfo *ri = a->reg_info ? a->reg_info(out->reg.cls, out->reg.num, C55_SUB_NONE) : NULL;
	out->width = ri ? ri->width : 0;
}
// Special-register destination of "mov #k16, <reg>" (opcode 0x78): byte3 bit 0
// is don't-care; byte3 bits 1-4 select the register -- 0 dp, 1 ssp, 2 cdp,
// 3 bsa01, 4 bsa23, 5 bsa45, 6 bsa67, 7 bsac, 8 sp. Selectors 9-15 are
// unassigned and left to the legacy decoder.
static void c55x_x_78dst(const C55ArchDesc *a, ut64 bits, const C55OpDesc *d, C55Operand *out) {
	ut8 spr;
	switch ((ut8)((bits >> d->lo) & 0x0f)) {
	case 0: spr = C55X_SPR_DP; break;
	case 1: spr = C55X_SPR_SSP; break;
	case 2: spr = C55X_SPR_CDP; break;
	case 3: spr = C55X_SPR_BSA01; break;
	case 4: spr = C55X_SPR_BSA23; break;
	case 5: spr = C55X_SPR_BSA45; break;
	case 6: spr = C55X_SPR_BSA67; break;
	case 7: spr = C55X_SPR_BSAC; break;
	case 8: spr = C55X_SPR_SP; break;
	default:
		out->kind = C55_OP_INVALID;
		return;
	}
	out->kind = C55_OP_REG;
	out->reg.cls = C55_RC_SPECIAL;
	out->reg.num = spr;
	out->reg.sub = C55_SUB_NONE;
	const C55RegInfo *ri = a->reg_info ? a->reg_info(out->reg.cls, out->reg.num, C55_SUB_NONE) : NULL;
	out->width = ri ? ri->width : 16;
}

// Destination register selector for mov #k12, <reg> (opcode 0x16): byte2's low
// nibble picks a control register; the immediate field sits in the 12 bits
// above it. Only eight encodings are defined (the rest are invalid).
static int c55x_16dst_spr(ut8 sel) {
	switch (sel) {
	case 0x0: return C55X_SPR_DPH;
	case 0x3: return C55X_SPR_PDP;
	case 0x4: return C55X_SPR_BK03;
	case 0x5: return C55X_SPR_BK47;
	case 0x6: return C55X_SPR_BKC;
	case 0x8: return C55X_SPR_CSR;
	case 0x9: return C55X_SPR_BRC0;
	case 0xa: return C55X_SPR_BRC1;
	default: return -1;
	}
}
static void c55x_x_16dst(const C55ArchDesc *a, ut64 bits, const C55OpDesc *d, C55Operand *out) {
	int spr = c55x_16dst_spr((ut8)((bits >> d->lo) & 0x0f));
	if (spr < 0) {
		out->kind = C55_OP_INVALID;
		return;
	}
	out->kind = C55_OP_REG;
	out->reg.cls = C55_RC_SPECIAL;
	out->reg.num = (ut8)spr;
	out->reg.sub = C55_SUB_NONE;
	const C55RegInfo *ri = a->reg_info ? a->reg_info(out->reg.cls, out->reg.num, C55_SUB_NONE) : NULL;
	out->width = ri ? ri->width : 16;
}

// Immediate for mov #k12, <reg> (opcode 0x16): a 12-bit field (d->lo .. d->lo+11)
// masked and displayed at the destination register's width (capped at 12). The
// destination selector is the nibble just below the field (byte2 low nibble).
static void c55x_x_16imm(const C55ArchDesc *a, ut64 bits, const C55OpDesc *d, C55Operand *out) {
	int spr = c55x_16dst_spr((ut8)((bits >> (d->lo - 4)) & 0x0f));
	if (spr < 0) {
		out->kind = C55_OP_INVALID;
		return;
	}
	const C55RegInfo *ri = a->reg_info ? a->reg_info(C55_RC_SPECIAL, (ut8)spr, C55_SUB_NONE) : NULL;
	int rw = ri ? ri->width : 16;
	int iw = rw < 12 ? rw : 12;
	ut64 k = (bits >> d->lo) & 0xfff;
	out->kind = C55_OP_IMM;
	out->imm = k & (((ut64)1 << iw) - 1);
	out->width = iw;
}

// The implicit stack-pointer destination of "aadd #k8, sp" (opcode 0x4e): no
// bits select it, so the extractor always yields sp.
static void c55x_x_sp(const C55ArchDesc *a, ut64 bits, const C55OpDesc *d, C55Operand *out) {
	(void)bits;
	(void)d;
	out->kind = C55_OP_REG;
	out->reg.cls = C55_RC_SP;
	out->reg.num = 0;
	out->reg.sub = C55_SUB_NONE;
	const C55RegInfo *ri = a->reg_info ? a->reg_info(C55_RC_SP, 0, C55_SUB_NONE) : NULL;
	out->width = ri ? ri->width : 16;
}

// Special-register destination of the "mov gr4, <reg>" forms (opcode 0x52, low
// nibble 8-15): 8 -> sp, 9 -> ssp, 10 -> cdp, 12 -> csr, 13 -> brc1, 14 -> brc0.
// Low nibbles 11 and 15 are unassigned and left to the legacy decoder. (The map
// differs from the 0x44 source map: e.g. 14 is brc0 here, rptc there.)
static void c55x_x_52dst(const C55ArchDesc *a, ut64 bits, const C55OpDesc *d, C55Operand *out) {
	ut8 spr;
	switch ((ut8)((bits >> d->lo) & 0x0f)) {
	case 8: spr = C55X_SPR_SP; break;
	case 9: spr = C55X_SPR_SSP; break;
	case 10: spr = C55X_SPR_CDP; break;
	case 12: spr = C55X_SPR_CSR; break;
	case 13: spr = C55X_SPR_BRC1; break;
	case 14: spr = C55X_SPR_BRC0; break;
	default:
		out->kind = C55_OP_INVALID;
		return;
	}
	out->kind = C55_OP_REG;
	out->reg.cls = C55_RC_SPECIAL;
	out->reg.num = spr;
	out->reg.sub = C55_SUB_NONE;
	const C55RegInfo *sri = a->reg_info ? a->reg_info(out->reg.cls, out->reg.num, C55_SUB_NONE) : NULL;
	out->width = sri ? sri->width : 0;
}

// swap / swapp register pair (opcode 0x5e). The byte-1 low nibble selects the
// pair; d->param 0 yields the first register, 1 the second. Byte-1 bit 4 marks
// swapp (the dual exchange), for which only the even-base pairs are valid; the
// lifter swaps the following pair too (signalled by the row's `.both`).
static void c55x_x_swap(const C55ArchDesc *a, ut64 bits, const C55OpDesc *d, C55Operand *out) {
	static const struct {
		ut8 c0, n0, c1, n1;
		bool ok;
	} pairs[16] = {
		[0] = { C55_RC_AC, 0, C55_RC_AC, 2, true },
		[1] = { C55_RC_AC, 1, C55_RC_AC, 3, true },
		[4] = { C55_RC_T, 0, C55_RC_T, 2, true },
		[5] = { C55_RC_T, 1, C55_RC_T, 3, true },
		[8] = { C55_RC_AR, 0, C55_RC_AR, 2, true },
		[9] = { C55_RC_AR, 1, C55_RC_AR, 3, true },
		[12] = { C55_RC_AR, 4, C55_RC_T, 0, true },
		[13] = { C55_RC_AR, 5, C55_RC_T, 1, true },
		[14] = { C55_RC_AR, 6, C55_RC_T, 2, true },
		[15] = { C55_RC_AR, 7, C55_RC_T, 3, true },
	};
	ut8 sel = (ut8)((bits >> d->lo) & 0x0f);
	bool swapp = ((bits >> 4) & 1) != 0;
	bool swap4 = ((bits >> 5) & 1) != 0;
	// key 56 (both the swapp and swap4 marker bits set, sel == 8): the
	// standalone "swap AR0, AR1" -- the single arbitrary AR pair, distinct
	// from the sel-8 pair(AR0, AR2).
	if (swapp && swap4 && sel == 8) {
		out->kind = C55_OP_REG;
		out->reg.sub = C55_SUB_NONE;
		out->reg.cls = C55_RC_AR;
		out->reg.num = d->param == 0 ? 0 : 1;
		const C55RegInfo *ri = a->reg_info ? a->reg_info(out->reg.cls, out->reg.num, C55_SUB_NONE) : NULL;
		out->width = ri ? ri->width : 0;
		return;
	}
	if (!pairs[sel].ok) {
		out->kind = C55_OP_INVALID;
		return;
	}
	if (swap4) {
		// swap4 only exchanges the ar4/ar5/ar6/ar7 <-> t0/t1/t2/t3 quad.
		if (sel != 12) {
			out->kind = C55_OP_INVALID;
			return;
		}
	} else if (swapp && (sel & 1)) {
		out->kind = C55_OP_INVALID;
		return;
	}
	out->kind = C55_OP_REG;
	out->reg.sub = C55_SUB_NONE;
	if (d->param == 0) {
		out->reg.cls = pairs[sel].c0;
		out->reg.num = pairs[sel].n0;
	} else {
		out->reg.cls = pairs[sel].c1;
		out->reg.num = pairs[sel].n1;
	}
	const C55RegInfo *ri = a->reg_info ? a->reg_info(out->reg.cls, out->reg.num, C55_SUB_NONE) : NULL;
	out->width = ri ? ri->width : 0;
}
// 0x50, sub-opcodes 0/1). Sub-opcode bit 0 selects the direction: 0 -> +1,
// 1 -> -1. Rendered as a signed decimal with a '#' prefix; lifted by the shared
// SFTL shift handler (a positive count shifts left, a negative one right).
static void c55x_x_sftl_imm(const C55ArchDesc *a, ut64 bits, const C55OpDesc *d, C55Operand *out) {
	(void)a;
	(void)d;
	out->kind = C55_OP_IMM;
	out->imm = (bits & 1) ? (ut64)(-1) : 1;
	out->width = 6;
	out->imm_signed = true;
	out->hash_dec = true;
}

// The fixed shift count of the "sfts dst, #1" / "sfts dst, #-1" forms (opcode
// 0x44, high nibble 4-7). The high-nibble low bit (byte1 bit 4) selects the
// direction, with the opposite polarity to sftl: 1 -> +1, 0 -> -1. Rendered as
// a signed decimal with '#'; lifted (arithmetically) by the shared SFTS handler.
static void c55x_x_sfts_imm(const C55ArchDesc *a, ut64 bits, const C55OpDesc *d, C55Operand *out) {
	(void)a;
	(void)d;
	out->kind = C55_OP_IMM;
	out->imm = ((bits >> 4) & 1) ? 1 : (ut64)(-1);
	out->width = 6;
	out->imm_signed = true;
	out->hash_dec = true;
}

// Extended-register ("xdst" / "xsrc") field of the popboth / pshboth pair stack
// ops (opcode 0x50, sub-opcodes 4/5): 0-3 -> AC0-AC3, 4 -> XSP, 5 -> XSSP,
// 6 -> XDP, 7 -> XCDP, 8-15 -> XAR0-XAR7. These move a register pair; the
// shared push/pop lifter leaves them unlifted (see the .both flag).
static void c55x_x_xgr4(const C55ArchDesc *a, ut64 bits, const C55OpDesc *d, C55Operand *out) {
	ut8 v = (ut8)((bits >> d->lo) & 0x0f);
	out->kind = C55_OP_REG;
	out->reg.sub = C55_SUB_NONE;
	if (v < 4) {
		out->reg.cls = C55_RC_AC;
		out->reg.num = v;
	} else if (v < 8) {
		out->reg.cls = C55_RC_SPECIAL;
		out->reg.num = (ut8)(C55X_SPR_XSP + (v - 4)); // XSP, XSSP, XDP, XCDP
	} else {
		out->reg.cls = C55_RC_XAR;
		out->reg.num = (ut8)(v - 8);
	}
	const C55RegInfo *ri = a->reg_info ? a->reg_info(out->reg.cls, out->reg.num, C55_SUB_NONE) : NULL;
	out->width = ri ? ri->width : 0;
}

// Source gr4 register that collapses against the destination gr4 register when
// the two are equal -- the optional "src" of the "add/sub Smem, [src,] dst"
// forms (opcodes 0xd6/0xd7), where src is the last byte's bits 0-3 and dst its
// bits 4-7. When src == dst only the destination is printed (dst += Smem).
static void c55x_x_gr4_src(const C55ArchDesc *a, ut64 bits, const C55OpDesc *d, C55Operand *out) {
	ut8 src = (ut8)((bits >> d->lo) & 0x0f);
	ut8 dst = (ut8)((bits >> (d->lo + 4)) & 0x0f);
	if (src == dst) {
		out->kind = C55_OP_NONE;
		return;
	}
	out->kind = C55_OP_REG;
	c55x_gr4(src, &out->reg);
	const C55RegInfo *ri = a->reg_info ? a->reg_info(out->reg.cls, out->reg.num, C55_SUB_NONE) : NULL;
	out->width = ri ? ri->width : 0;
}

// The optional "ACx" source of the dbl add/sub forms (opcode 0xed): SS is the
// last byte's bits 7-6 (read at d->lo) and DD the destination at bits 5-4
// (d->lo - 2). Both name an accumulator ac0-3; when SS == DD only the
// destination prints (ACy += dbl(Lmem)).
static void c55x_x_ac_src2(const C55ArchDesc *a, ut64 bits, const C55OpDesc *d, C55Operand *out) {
	ut8 src = (ut8)((bits >> d->lo) & 0x3);
	ut8 dst = (ut8)((bits >> (d->lo - 2)) & 0x3);
	if (src == dst) {
		out->kind = C55_OP_NONE;
		return;
	}
	out->kind = C55_OP_REG;
	out->reg.cls = C55_RC_AC;
	out->reg.num = src;
	out->reg.sub = C55_SUB_NONE;
	const C55RegInfo *ri = a->reg_info ? a->reg_info(out->reg.cls, out->reg.num, C55_SUB_NONE) : NULL;
	out->width = ri ? ri->width : 0;
}

// Condition field (xcc / xccpart, and the conditional transfers): the low seven
// bits encode either a register-versus-zero comparison or a status-flag
// expression. The subject register is reg-field-4 in bits 0-3 and the
// comparison is bits 4-6 (0..5 -> == != < <= > >=, matching C55Relop). The flag
// expressions (bits 4-6 of 6 or 7) are not represented here yet, so the decode
// is abandoned for them and the legacy front-end renders those conditions.
static void c55x_x_cond(const C55ArchDesc *a, ut64 bits, const C55OpDesc *d, C55Operand *out) {
	(void)a;
	ut8 field = (ut8)((bits >> d->lo) & 0x7f);
	ut8 cmp = (ut8)((field >> 4) & 0x07);
	if (cmp <= C55_REL_GE) {
		// 0-5: a register compared against zero.
		out->kind = C55_OP_COND;
		c55x_gr4((ut8)(field & 0x0f), &out->reg);
		out->relop = (C55Relop)cmp;
		out->imm = 0;
		return;
	}
	// 6-7: a status-flag condition. The flag id is (cmp-6)*16 + the low nibble,
	// which indexes the shared cond_flags table. C55x leaves a few of those
	// slots undefined - overflow(govf) (7), word/byte mode (12/13), two reserved
	// (14/15) and !overflow(govf) (23) - so those fall back to the legacy
	// decoder rather than decoding as a flag.
	ut8 flag = (ut8)(((cmp - 6) << 4) | (field & 0x0f));
	if (flag == 7 || (flag >= 12 && flag <= 15) || flag == 23) {
		out->kind = C55_OP_INVALID;
		return;
	}
	out->kind = C55_OP_COND;
	out->cond_is_flag = true;
	out->cond_flag = flag;
}

// bcc short form (opcodes 0x60-0x67): the destination is a forward offset in a
// four-bit field whose high three bits are byte0 bits 0-2 and whose low bit is
// byte1 bit 7 (so the field occupies packed bits 7-10). The branch goes to
// pc + size + offset; the offset is small and always non-negative, so it is
// rendered as a one-digit immediate and flagged unsigned (no sign-extension).
static void c55x_x_bcc_short_target(const C55ArchDesc *a, ut64 bits, const C55OpDesc *d, C55Operand *out) {
	(void)a;
	out->kind = C55_OP_IMM;
	out->imm = (ut64)((bits >> d->lo) & 0x0f);
	out->width = 4;
	out->reltarget = true;
	out->reltarget_unsigned = true;
}

// Compare-and-branch condition (bcc 0x6f): the source register is gr4 in byte1
// bits 4-7 and the comparison is byte1 bits 2-3 (0 ==, 1 <, 2 >=, 3 !=), with
// bit 0 selecting the unsigned form (bccu). byte2 is the 8-bit compare constant
// K8. The 16-bit field passed in is byte1:byte2. Only the signed comparisons
// are represented here; the unsigned form is left to the legacy decoder.
static void c55x_x_cond_imm(const C55ArchDesc *a, ut64 bits, const C55OpDesc *d, C55Operand *out) {
	(void)a;
	static const C55Relop relmap[4] = { C55_REL_EQ, C55_REL_LT, C55_REL_GE, C55_REL_NE };
	ut16 field = (ut16)((bits >> d->lo) & 0xffff);
	ut8 b1 = (ut8)(field >> 8);
	if (b1 & 1) {
		out->kind = C55_OP_INVALID;
		return;
	}
	out->kind = C55_OP_COND;
	c55x_gr4((ut8)(b1 >> 4), &out->reg);
	out->relop = relmap[(b1 >> 2) & 3];
	out->imm = (ut64)(field & 0xff);
	out->width = 8;
	out->cmp_imm = true;
}

// 2-bit accumulator selector for the register-indirect control-transfer forms
// (b acx / call acx): only the four accumulators are addressable as a branch or
// call target, encoded in the low two bits of the operand byte (the remaining
// bits are don't-cares -- 0x9104, 0x9108, ... all decode to "b ac0").
static void c55x_x_ac2(const C55ArchDesc *a, ut64 bits, const C55OpDesc *d, C55Operand *out) {
	out->kind = C55_OP_REG;
	out->reg.sub = C55_SUB_NONE;
	out->reg.cls = C55_RC_AC;
	out->reg.num = (ut8)((bits >> d->lo) & 0x3);
	const C55RegInfo *ri = a->reg_info ? a->reg_info(out->reg.cls, out->reg.num, C55_SUB_NONE) : NULL;
	out->width = ri ? ri->width : 0;
}

// Destination accumulator ACy for mpyk/mpykr/mack/mackr: identical to
// c55x_x_ac2 but flagged so the formatter omits it when it equals the source
// ACx (the TI "mpyk #k, ACx" short form where ACy defaults to ACx).
static void c55x_x_ac2_elide(const C55ArchDesc *a, ut64 bits, const C55OpDesc *d, C55Operand *out) {
	c55x_x_ac2(a, bits, d, out);
	out->elide_if_eq_prev = true;
}

// 2-bit Tx selector (T0-T3) for the mack/mackr coefficient register.
static void c55x_x_t2(const C55ArchDesc *a, ut64 bits, const C55OpDesc *d, C55Operand *out) {
	out->kind = C55_OP_REG;
	out->reg.sub = C55_SUB_NONE;
	out->reg.cls = C55_RC_T;
	out->reg.num = (ut8)((bits >> d->lo) & 0x3);
	const C55RegInfo *ri = a->reg_info ? a->reg_info(out->reg.cls, out->reg.num, C55_SUB_NONE) : NULL;
	out->width = ri ? ri->width : 0;
}

// A left-shifted 16-bit immediate: the "#k16 << #16" operand of the opcode-0x7a
// immediate-ALU forms. The shift is a fixed 16 and is rendered/lifted as such.
static void c55x_x_imm_sh16(const C55ArchDesc *a, ut64 bits, const C55OpDesc *d, C55Operand *out) {
	c55_x_imm(a, bits, d, out);
	out->sh_left = true;
	out->shamt = 16;
}

// A left-shifted 16-bit immediate with a variable shift count: the
// "#k16 << #sh" operand of the opcode-0x70..0x74 immediate-ALU forms. The shift
// is byte3 bits 0-3 (0-15) and is rendered as hex.
static void c55x_x_imm_varsh(const C55ArchDesc *a, ut64 bits, const C55OpDesc *d, C55Operand *out) {
	c55_x_imm(a, bits, d, out);
	out->sh_left = true;
	out->shamt = (int8_t)(bits & 0xf);
	out->shamt_hex = true;
}

// A negated 4-bit magnitude immediate: the "-#k" operand of the opcode-0x3e
// short move (byte1 high nibble is the magnitude k, 0-15; the value is -k).
static void c55x_x_negk4(const C55ArchDesc *a, ut64 bits, const C55OpDesc *d, C55Operand *out) {
	(void)a;
	ut8 k = (ut8)((bits >> d->lo) & 0xf);
	out->kind = C55_OP_IMM;
	out->imm = (ut64)(-(st64)k);
	out->imm_signed = true;
	out->neg_imm = true;
	out->width = 4;
}

// Fixed TC1 / TC2 literal operands (the addsub2cc form always tests both test-
// control flags, rendered as the constant "TC1, TC2" pair).
static void c55x_x_tc1(const C55ArchDesc *a, ut64 bits, const C55OpDesc *d, C55Operand *out) {
	(void)a;
	(void)bits;
	(void)d;
	out->kind = C55_OP_REG;
	out->reg.cls = C55_RC_TC;
	out->reg.num = 0;
	out->reg.sub = C55_SUB_NONE;
	out->width = 1;
}
static void c55x_x_tc2(const C55ArchDesc *a, ut64 bits, const C55OpDesc *d, C55Operand *out) {
	(void)a;
	(void)bits;
	(void)d;
	out->kind = C55_OP_REG;
	out->reg.cls = C55_RC_TC;
	out->reg.num = 1;
	out->reg.sub = C55_SUB_NONE;
	out->width = 1;
}

// The "Baddr" bit-address operand of btstp: the legacy disassembler does not
// decode the bit address and renders the literal placeholder "Baddr". The
// operand carries a verbatim render string so the output matches byte-for-byte.
static void c55x_x_baddr(const C55ArchDesc *a, ut64 bits, const C55OpDesc *d, C55Operand *out) {
	(void)a;
	(void)bits;
	(void)d;
	out->kind = C55_OP_IMM;
	out->raw = "Baddr";
}

// High accumulator-half source ACx.h for mov hi(ACx), Smem (opcodes 0xbc-0xbf):
// the accumulator number is the opcode's low two bits.
static void c55x_x_achi(const C55ArchDesc *a, ut64 bits, const C55OpDesc *d, C55Operand *out) {
	out->kind = C55_OP_REG;
	out->reg.cls = C55_RC_AC;
	out->reg.num = (ut8)((bits >> d->lo) & 0x3);
	out->reg.sub = C55_SUB_HI;
	const C55RegInfo *ri = a->reg_info ? a->reg_info(out->reg.cls, out->reg.num, C55_SUB_NONE) : NULL;
	out->width = ri ? ri->width : 0;
}

// Single data-memory (Smem) operand for the C55x load / store group. The
// operand byte selects the addressing mode: an even byte is an SP-relative
// direct access (*sp(#k), k = byte>>1) and an odd byte selects a register mode
// whose base ARn is byte[7:5] and whose sub-mode is byte[4:1]. The SP-relative
// direct forms and the 2-byte register-modify matrix the shared
// effective-address / memory primitives understand are decoded here; the
// const-indexed and absolute forms (which extend the instruction with a 16-bit
// field) and the pre-modify and bit-reverse forms fall back to the legacy
// decoder.
static void c55x_x_smem(const C55ArchDesc *a, ut64 bits, const C55OpDesc *d, C55Operand *out) {
	(void)a;
	ut8 b = (ut8)((bits >> d->lo) & 0xff);
	if (!(b & 1)) {
		// SP-relative direct: *sp(#k), the unsigned offset k = byte>>1 (0-127).
		out->kind = C55_OP_MEM;
		out->access = 16;
		out->amode = C55_AM_INDEXED;
		out->reg.cls = C55_RC_SP;
		out->reg.num = 0;
		out->reg.sub = C55_SUB_NONE;
		out->disp = (st32)(b >> 1);
		return;
	}
	ut8 mode = (ut8)((b >> 1) & 0x0f);
	out->kind = C55_OP_MEM;
	out->access = 16;
	c55x_gr4((ut8)(8 + ((b >> 5) & 7)), &out->reg); // base ARn
	switch (mode) {
	case 0: out->amode = C55_AM_INDIRECT; return;
	case 1: out->amode = C55_AM_POSTINC; return;
	case 2: out->amode = C55_AM_POSTDEC; return;
	case 3:
		out->amode = C55_AM_POSTADD;
		c55x_gr4(4, &out->index); // t0
		return;
	case 4:
		out->amode = C55_AM_POSTSUB;
		c55x_gr4(4, &out->index); // t0
		return;
	case 5:
		out->amode = C55_AM_IDXREG;
		c55x_gr4(4, &out->index); // t0
		return;
	case 9:
		out->amode = C55_AM_POSTADD;
		c55x_gr4(5, &out->index); // t1
		return;
	case 10:
		out->amode = C55_AM_POSTSUB;
		c55x_gr4(5, &out->index); // t1
		return;
	case 11:
		out->amode = C55_AM_IDXREG;
		c55x_gr4(5, &out->index); // t1
		return;
	case 12: out->amode = C55_AM_PREINC; return; // *+arN
	case 13: out->amode = C55_AM_PREDEC; return; // *-arN
	case 14:
		out->amode = C55_AM_BITREV; // *(arN + t0b) reverse-carry
		c55x_gr4(4, &out->index); // t0 (rendered t0b)
		return;
	case 15:
		out->amode = C55_AM_BITREV_SUB; // *(arN - t0b) reverse-carry
		c55x_gr4(4, &out->index); // t0 (rendered t0b)
		return;
	case 6:
		// *arN(#K16) long const-index: ARn is the base pointer (unmodified); the
		// signed 16-bit constant lives in a 2-byte extension that c55_decode
		// appends and writes into ->disp.
		out->amode = C55_AM_CONST_IDX;
		return;
	case 7:
		// *+arN(#K16) long const-index with pre-modify: like mode 6 but ARn is
		// updated (ARn += K16); the 2-byte extension is read by c55_decode.
		out->amode = C55_AM_CONST_IDX_PRE;
		return;
	case 8: {
		// Mode 8 dispatches on the base ARn field: 0 -> abs16(#k16), 3 -> *cdp,
		// 4 -> *cdp+, 5 -> *cdp-, 6 -> *cdp(K16), 7 -> *+cdp(K16). For abs16 the
		// address is DPH:k16 so the base is irrelevant; c55_decode reads the
		// unsigned 2-byte k16. Base 1 (*(k23)) and base 2 (port(k16)) are not
		// modelled here and fall through to the legacy decoder.
		ut8 base = (b >> 5) & 7;
		if (base >= 3) {
			out->reg.cls = C55_RC_CDP;
			out->reg.num = 0;
			out->reg.sub = C55_SUB_NONE;
			switch (base) {
			case 4: out->amode = C55_AM_POSTINC; return; // *cdp+
			case 5: out->amode = C55_AM_POSTDEC; return; // *cdp-
			case 6: out->amode = C55_AM_CONST_IDX; return; // *cdp(K16)
			case 7: out->amode = C55_AM_CONST_IDX_PRE; return; // *+cdp(K16)
			default: out->amode = C55_AM_INDIRECT; return; // 3 -> *cdp
			}
		}
		if (base == 1) {
			// *(k23): the 23-bit (k24-encoded) absolute byte address lives in a
			// 3-byte extension that c55_decode appends and writes into abs_addr.
			// (The legacy decoder renders this address incorrectly -- it leaks
			// format-string bytes -- so the shared path supersedes it here.)
			out->amode = C55_AM_ABSOLUTE;
			out->abs_addr = C55_ABS_EXT;
			return;
		}
		out->amode = C55_AM_ABS16;
		return;
	}
	default:
		// all Smem modes (0-15) are handled above; this is defensive only.
		out->kind = C55_OP_INVALID;
		return;
	}
}

// Smem with a register shift count: the "Smem << Tx" forms (opcode 0xdd). The
// Smem byte is decoded as usual; the shift register Tx is the last byte's bits
// 2-3 (T0-T3), recorded in the dedicated shift-register field so it does not
// collide with any addressing index the Smem mode itself uses.
static void c55x_x_smem_shtx(const C55ArchDesc *a, ut64 bits, const C55OpDesc *d, C55Operand *out) {
	c55x_x_smem(a, bits, d, out);
	if (out->kind != C55_OP_MEM) {
		return; // invalid / instruction-extending Smem mode -> leave for the legacy
	}
	out->sh_mem_reg.cls = C55_RC_T;
	out->sh_mem_reg.num = (ut8)((bits >> 2) & 0x3);
	out->sh_mem_reg.sub = C55_SUB_NONE;
	out->sh_mem_reg_set = true;
}

// As c55x_x_smem_shtx, but also reads the rounding bit (byte2 bit 6) so the
// formatter wraps the operand in rnd(...) (the "mov rnd(Smem << Tx), ACx" form).
static void c55x_x_smem_shtx_rnd(const C55ArchDesc *a, ut64 bits, const C55OpDesc *d, C55Operand *out) {
	c55x_x_smem_shtx(a, bits, d, out);
	if (out->kind != C55_OP_MEM) {
		return;
	}
	out->mem_round = (bits >> 6) & 1;
}

// Smem source shifted left by 16 for the mov Smem << #16, ACx load group
// (opcodes 0xb0-0xb3): reuses the Smem decode and tags the operand with the
// fixed << 16 shift. The shift is rendered but, like the legacy decoder, the
// load is not yet lifted (the shared lifter skips a shifted memory access).
static void c55x_x_smem_sh16(const C55ArchDesc *a, ut64 bits, const C55OpDesc *d, C55Operand *out) {
	c55x_x_smem(a, bits, d, out);
	if (out->kind == C55_OP_MEM) {
		out->sh_left = true;
		out->shamt = 16;
	}
}

// Smem source carrying the "unsigned" qualifier from byte2 bit 0, rendered
// uns(...) by the memory formatter. Used by the 0xdf add/sub Smem forms, where
// bit 0 selects between a signed and an unsigned memory operand.
static void c55x_x_smem_uns(const C55ArchDesc *a, ut64 bits, const C55OpDesc *d, C55Operand *out) {
	c55x_x_smem(a, bits, d, out);
	if (out->kind == C55_OP_MEM) {
		out->uns = (bits & 1) != 0;
	}
}

// Smem source with a byte-access wrapper (d->param: 1 high_byte, 2 low_byte)
// plus the unsigned qualifier from byte2 bit 0. Used by the 0xdf mov
// high_byte/low_byte forms. The byte-access load is not lifted (the memory
// mover declines a byte_sel operand), matching the legacy decoder.
static void c55x_x_smem_byte(const C55ArchDesc *a, ut64 bits, const C55OpDesc *d, C55Operand *out) {
	c55x_x_smem(a, bits, d, out);
	if (out->kind == C55_OP_MEM) {
		out->byte_sel = (ut8)d->param;
		out->uns = (bits & 1) != 0;
	}
}

// Destination special register of the 0xdc mov Smem, <special-reg> forms,
// selected by byte2 bits 4-7. d->param picks the sub-group: 2 is the
// dp/cdp/bsa*/sp/ssp/bk*/dph/pdp set (4-bit selector), 3 is the
// csr/brc*/trn* set (3-bit selector). Undefined selectors become INVALID so
// the instruction falls through to the legacy decoder.
static void c55x_x_dc_movdst(const C55ArchDesc *a, ut64 bits, const C55OpDesc *d, C55Operand *out) {
	static const int8_t g2[16] = {
		C55X_SPR_DP, C55X_SPR_CDP, C55X_SPR_BSA01, C55X_SPR_BSA23,
		C55X_SPR_BSA45, C55X_SPR_BSA67, C55X_SPR_BSAC, C55X_SPR_SP,
		C55X_SPR_SSP, C55X_SPR_BK03, C55X_SPR_BK47, C55X_SPR_BKC,
		C55X_SPR_DPH, -1, -1, C55X_SPR_PDP
	};
	static const int8_t g3[8] = {
		C55X_SPR_CSR, C55X_SPR_BRC0, C55X_SPR_BRC1, C55X_SPR_TRN0,
		C55X_SPR_TRN1, -1, -1, -1
	};
	int8_t spr = (d->param == 2) ? g2[(bits >> 4) & 0xf] : g3[(bits >> 4) & 0x7];
	if (spr < 0) {
		out->kind = C55_OP_INVALID;
		return;
	}
	out->kind = C55_OP_REG;
	out->reg.cls = C55_RC_SPECIAL;
	out->reg.num = (ut8)spr;
	out->reg.sub = C55_SUB_NONE;
	const C55RegInfo *ri = a->reg_info ? a->reg_info(out->reg.cls, out->reg.num, C55_SUB_NONE) : NULL;
	out->width = ri ? ri->width : 16;
}

// A double-word (32-bit) single-data-memory operand: decoded like an Smem but
// rendered dbl(...) and treated as a two-word access (e.g. psh / pop dbl).
static void c55x_x_smem_dbl(const C55ArchDesc *a, ut64 bits, const C55OpDesc *d, C55Operand *out) {
	c55x_x_smem(a, bits, d, out);
	if (out->kind == C55_OP_MEM) {
		out->dbl = true;
		out->access = 32;
	}
}

// Long (dual) data-memory operand printed as "dual(Smem)": the dual-operand
// add/sub forms (addsub / subadd Tx, dual(Lmem), ACy) access a 32-bit long
// word and wrap the Smem in dual(...) rather than dbl(...).
static void c55x_x_smem_dual(const C55ArchDesc *a, ut64 bits, const C55OpDesc *d, C55Operand *out) {
	c55x_x_smem(a, bits, d, out);
	if (out->kind == C55_OP_MEM) {
		out->dual_wrap = true;
		out->access = 32;
	}
}

// MPY register form (opcode 0x58): the operand fields live in the low byte --
// Tx (bits 2-3), the source accumulator ACy (bits 4-5) and the destination
// accumulator ACx (bits 6-7). The disassembly is "mpy Tx, [ACy,] ACx": the
// middle source accumulator is printed only when it differs from the
// destination, so the source extractor yields C55_OP_NONE (which the decode
// loop skips) when the two coincide, giving the 2-operand form "mpy Tx, ACx".
static void c55x_x_mpy_t(const C55ArchDesc *a, ut64 bits, const C55OpDesc *d, C55Operand *out) {
	out->kind = C55_OP_REG;
	out->reg.sub = C55_SUB_NONE;
	out->reg.cls = C55_RC_T;
	out->reg.num = (ut8)((bits >> 2) & 0x3);
	const C55RegInfo *ri = a->reg_info ? a->reg_info(out->reg.cls, out->reg.num, C55_SUB_NONE) : NULL;
	out->width = ri ? ri->width : 0;
}

static void c55x_x_mpy_acsrc(const C55ArchDesc *a, ut64 bits, const C55OpDesc *d, C55Operand *out) {
	ut8 src = (ut8)((bits >> 4) & 0x3);
	ut8 dst = (ut8)((bits >> 6) & 0x3);
	if (src == dst) {
		out->kind = C55_OP_NONE;
		return;
	}
	out->kind = C55_OP_REG;
	out->reg.sub = C55_SUB_NONE;
	out->reg.cls = C55_RC_AC;
	out->reg.num = src;
	const C55RegInfo *ri = a->reg_info ? a->reg_info(out->reg.cls, out->reg.num, C55_SUB_NONE) : NULL;
	out->width = ri ? ri->width : 0;
}

static void c55x_x_mpy_acdst(const C55ArchDesc *a, ut64 bits, const C55OpDesc *d, C55Operand *out) {
	out->kind = C55_OP_REG;
	out->reg.sub = C55_SUB_NONE;
	out->reg.cls = C55_RC_AC;
	out->reg.num = (ut8)((bits >> 6) & 0x3);
	const C55RegInfo *ri = a->reg_info ? a->reg_info(out->reg.cls, out->reg.num, C55_SUB_NONE) : NULL;
	out->width = ri ? ri->width : 0;
}

// Trailing destination ACy (bits 6-7) of the register MAC (opcode 0x56, mac
// form): the "mac[r] ACx, Tx, ACy[, ACy]" syntax prints the explicit second ACy
// only when it differs from the ACx multiplicand (bits 4-5). When they coincide
// the operand collapses (C55_OP_NONE) and the three-operand form is rendered;
// either way the lifter folds the accumulate into the single destination ACy.
static void c55x_x_macreg_acy_dst(const C55ArchDesc *a, ut64 bits, const C55OpDesc *d, C55Operand *out) {
	ut8 acx = (ut8)((bits >> 4) & 0x3);
	ut8 acy = (ut8)((bits >> 6) & 0x3);
	if (acx == acy) {
		out->kind = C55_OP_NONE;
		return;
	}
	out->kind = C55_OP_REG;
	out->reg.sub = C55_SUB_NONE;
	out->reg.cls = C55_RC_AC;
	out->reg.num = acy;
	const C55RegInfo *ri = a->reg_info ? a->reg_info(out->reg.cls, out->reg.num, C55_SUB_NONE) : NULL;
	out->width = ri ? ri->width : 0;
}

// SHIFTW: the 6-bit signed shift count immediate of the 0x10 register-ALU group
// (the last byte's bits 0-5). It is stored raw (unsigned) so the disassembler
// prints the field value as the legacy does; the lifter sign-extends it to
// choose the shift direction and magnitude.
static void c55x_x_shiftw(const C55ArchDesc *a, ut64 bits, const C55OpDesc *d, C55Operand *out) {
	(void)a;
	(void)d;
	out->kind = C55_OP_IMM;
	out->imm = bits & 0x3f;
	out->width = 6;
	out->imm_signed = false;
}

// As c55x_x_shiftw, but the field is the right-hand side of the "ACx << #SHIFTW"
// syntax used by the 0x10 and/or/xor/add/sub shift-and-combine forms, so it is
// rendered joined to the previous operand by " << " instead of a comma.
static void c55x_x_shiftw_shl(const C55ArchDesc *a, ut64 bits, const C55OpDesc *d, C55Operand *out) {
	c55x_x_shiftw(a, bits, d, out);
	out->shl_join = true;
}

// Tx shift-count operand of "add/sub ACx << Tx, ACy" (opcode 0x5a). The 2-bit
// field at d->lo selects t0..t3; it is rendered joined to the preceding ACx by
// " << " and read by the (register-count) ADDSHL/SUBSHL lifter.
static void c55x_x_tx_shl(const C55ArchDesc *a, ut64 bits, const C55OpDesc *d, C55Operand *out) {
	out->kind = C55_OP_REG;
	out->reg.cls = C55_RC_T;
	out->reg.num = (ut8)((bits >> d->lo) & 0x3);
	out->reg.sub = C55_SUB_NONE;
	out->shl_join = true;
	const C55RegInfo *ri = a->reg_info ? a->reg_info(out->reg.cls, out->reg.num, C55_SUB_NONE) : NULL;
	out->width = ri ? ri->width : 0;
}

// Status register STn_55 operand of bclr / bset (opcode 0x46): the 3-bit field
// at d->lo selects st0_55..st3_55; selectors 4-7 are unassigned -> INVALID.
static void c55x_x_st(const C55ArchDesc *a, ut64 bits, const C55OpDesc *d, C55Operand *out) {
	ut8 n = (ut8)((bits >> d->lo) & 0x7);
	if (n > 3) {
		out->kind = C55_OP_INVALID;
		return;
	}
	out->kind = C55_OP_REG;
	out->reg.cls = C55_RC_ST;
	out->reg.num = n;
	out->reg.sub = C55_SUB_NONE;
	const C55RegInfo *sri = a->reg_info ? a->reg_info(out->reg.cls, out->reg.num, C55_SUB_NONE) : NULL;
	out->width = sri ? sri->width : 0;
}

// Register-register compare condition of cmp/cmpand/cmpor (opcode 0x12),
// rendered "SRC <relop> DST". SRC is the gr4 at byte-1 bits 4-7, DST the gr4 at
// byte-2 bits 4-7, the relop is byte-1 bits 2-3 (0 ==, 1 <, 2 >=, 3 !=).
static void c55x_x_cmpcond(const C55ArchDesc *a, ut64 bits, const C55OpDesc *d, C55Operand *out) {
	(void)a;
	(void)d;
	static const C55Relop rel_map[4] = { C55_REL_EQ, C55_REL_LT, C55_REL_GE, C55_REL_NE };
	out->kind = C55_OP_COND;
	out->cmp_to_reg = true;
	out->relop = rel_map[(bits >> 10) & 0x3];
	c55x_gr4((ut8)((bits >> 12) & 0xf), &out->reg);
	c55x_gr4((ut8)((bits >> 4) & 0xf), &out->index);
}

// A TC status-flag operand (tc1/tc2, optionally negated as !tc1/!tc2) of the
// compare forms. The selecting bit is at d->lo (byte-2 bit 0 for the TCz
// output, bit 1 for the cmpand/cmpor TCx input); 0 -> tc1, 1 -> tc2. When
// d->param is non-zero it is the bit position of the negation flag (byte-2
// bit 3 for the TCx input); a set negation bit yields the !tcN condition id.
static void c55x_x_tcflag(const C55ArchDesc *a, ut64 bits, const C55OpDesc *d, C55Operand *out) {
	(void)a;
	ut8 tcb = (ut8)((bits >> d->lo) & 0x1);
	bool neg = d->param && ((bits >> d->param) & 0x1);
	out->kind = C55_OP_COND;
	out->cond_is_flag = true;
	out->cond_flag = (ut8)((neg ? 20 : 4) + tcb);
}

// rol/ror rotate-in / rotate-out bit selector: a single bit picking CARRY (0)
// or TC2 (1). Rendered via the shared cond-flag table (carry -> id 6, tc2 -> id
// 5); the lifter maps these ids to status-register bits 11 (carry) and 12 (tc2).
static void c55x_x_rolflag(const C55ArchDesc *a, ut64 bits, const C55OpDesc *d, C55Operand *out) {
	(void)a;
	ut8 b = (ut8)((bits >> d->lo) & 0x1);
	out->kind = C55_OP_COND;
	out->cond_is_flag = true;
	out->cond_flag = b ? 5 : 6; // 0 -> carry, 1 -> tc2
}

// Trailing destination ACy (bits 14-15) of the three-byte 0x10 register-ALU
// group, collapsing against the source ACx (bits 12-13) when the two are equal
// (mirrors c55x_x_macreg_acy_dst but for the wider three-byte encoding).
static void c55x_x_shiftk_acy(const C55ArchDesc *a, ut64 bits, const C55OpDesc *d, C55Operand *out) {
	ut8 acx = (ut8)((bits >> 12) & 0x3);
	ut8 acy = (ut8)((bits >> 14) & 0x3);
	if (acx == acy) {
		out->kind = C55_OP_NONE;
		return;
	}
	out->kind = C55_OP_REG;
	out->reg.sub = C55_SUB_NONE;
	out->reg.cls = C55_RC_AC;
	out->reg.num = acy;
	const C55RegInfo *ri = a->reg_info ? a->reg_info(out->reg.cls, out->reg.num, C55_SUB_NONE) : NULL;
	out->width = ri ? ri->width : 0;
}
// (uns(Cmem) -> zero-extended).
static void c55x_x_cmem(const C55ArchDesc *a, ut64 bits, const C55OpDesc *d, C55Operand *out) {
	out->kind = C55_OP_MEM;
	out->reg.cls = C55_RC_CDP;
	out->reg.num = 0;
	out->reg.sub = C55_SUB_NONE;
	out->access = 16;
	out->uns = d->param != 0;
	switch ((ut8)(bits & 0x3)) {
	case 0: out->amode = C55_AM_INDIRECT; break;
	case 1: out->amode = C55_AM_POSTINC; break;
	case 2: out->amode = C55_AM_POSTDEC; break;
	default: // 3: *(cdp+t0)
		out->amode = C55_AM_POSTADD;
		out->index.cls = C55_RC_T;
		out->index.num = 0;
		out->index.sub = C55_SUB_NONE;
		break;
	}
}

// Destination accumulator ACx of a memory multiply / MAC (last-byte bits 4-5).
static void c55x_x_mac_acdst(const C55ArchDesc *a, ut64 bits, const C55OpDesc *d, C55Operand *out) {
	out->kind = C55_OP_REG;
	out->reg.sub = C55_SUB_NONE;
	out->reg.cls = C55_RC_AC;
	out->reg.num = (ut8)((bits >> 4) & 0x3);
	const C55RegInfo *ri = a->reg_info ? a->reg_info(out->reg.cls, out->reg.num, C55_SUB_NONE) : NULL;
	out->width = ri ? ri->width : 0;
}

// Coefficient accumulator ACx (its high word ACx(32-16) is the multiplicand) of the
// accumulator-coefficient memory MACs: last-byte bits 0-1 select the source
// ACx, bits 4-5 the destination ACy. When the two coincide the legacy decoder
// renders the two-operand form but leaves it unlifted, so that case is reported
// as INVALID (the structured decode is abandoned to the legacy path) and only
// the distinct-register three-operand form is lifted here.
static void c55x_x_mac_accoef(const C55ArchDesc *a, ut64 bits, const C55OpDesc *d, C55Operand *out) {
	ut8 ss = (ut8)(bits & 0x3);
	ut8 dd = (ut8)((bits >> 4) & 0x3);
	if (ss == dd) {
		out->kind = C55_OP_INVALID;
		return;
	}
	out->kind = C55_OP_REG;
	out->reg.sub = C55_SUB_NONE;
	out->reg.cls = C55_RC_AC;
	out->reg.num = ss;
	const C55RegInfo *ri = a->reg_info ? a->reg_info(out->reg.cls, out->reg.num, C55_SUB_NONE) : NULL;
	out->width = ri ? ri->width : 0;
}

// Coefficient T register Tx of the Tx-coefficient memory MACs (macm / masm /
// mpym Smem, Tx, ...); the two-bit Tx selector is at the op-slot's `lo`.
static void c55x_x_mac_tcoef(const C55ArchDesc *a, ut64 bits, const C55OpDesc *d, C55Operand *out) {
	out->kind = C55_OP_REG;
	out->reg.sub = C55_SUB_NONE;
	out->reg.cls = C55_RC_T;
	out->reg.num = (ut8)((bits >> d->lo) & 0x3);
	const C55RegInfo *ri = a->reg_info ? a->reg_info(out->reg.cls, out->reg.num, C55_SUB_NONE) : NULL;
	out->width = ri ? ri->width : 0;
}

// mpymu Smem, Tx, ACx: the unsigned multiply (last byte's op bits = 11). The whole
// operation is unsigned, so both multiplicands carry uns; the 'u' mnemonic suffix
// (uns_all) renders them without per-operand uns() wrappers.
static void c55x_x_smem_u(const C55ArchDesc *a, ut64 bits, const C55OpDesc *d, C55Operand *out) {
	c55x_x_smem(a, bits, d, out);
	if (out->kind == C55_OP_MEM) {
		out->uns = true;
	}
}
static void c55x_x_tcoef_u(const C55ArchDesc *a, ut64 bits, const C55OpDesc *d, C55Operand *out) {
	c55x_x_mac_tcoef(a, bits, d, out);
	out->uns = true;
}

// Accumulator operand ACx (last-byte bits 0-1) that collapses against the
// destination ACy (bits 4-5): used both as the explicit accumulator source of
// the Tx-coefficient MACs and as the high-word ACx(32-16) coefficient of the
// accumulator-coefficient multiplies. When the two registers differ the
// four/three-operand form is rendered with ACx present; when they coincide the
// operand collapses (NONE) to the shorter form whose role defaults to ACy.
static void c55x_x_mac_accsrc(const C55ArchDesc *a, ut64 bits, const C55OpDesc *d, C55Operand *out) {
	ut8 ss = (ut8)(bits & 0x3);
	ut8 dd = (ut8)((bits >> 4) & 0x3);
	if (ss == dd) {
		out->kind = C55_OP_NONE;
		return;
	}
	out->kind = C55_OP_REG;
	out->reg.sub = C55_SUB_NONE;
	out->reg.cls = C55_RC_AC;
	out->reg.num = ss;
	const C55RegInfo *ri = a->reg_info ? a->reg_info(out->reg.cls, out->reg.num, C55_SUB_NONE) : NULL;
	out->width = ri ? ri->width : 0;
}

static const char *c55x_mnemonic(ut16 id) {
	return tms320c55x_insn_name((TMS320C55InsID)id);
}

static ut32 c55x_op_type(ut16 id) {
	switch ((TMS320C55InsID)id) {
	case TMS320C55_INS_NOP:
	case TMS320C55_INS_IDLE: return RZ_ANALYSIS_OP_TYPE_NOP;
	case TMS320C55_INS_MOV: return RZ_ANALYSIS_OP_TYPE_MOV;
	// delay Smem: a memory-delay that copies the addressed word to the next
	// higher address (TI SWPU104 6.7.1); the legacy analysis models it as a
	// data MOVE, so the shared path mirrors that op type.
	case TMS320C55_INS_DELAY: return RZ_ANALYSIS_OP_TYPE_MOV;
	// amar computes an effective address (and applies the addressing mode's
	// post-modify side effect) without accessing memory: an address load.
	case TMS320C55_INS_AMAR: return RZ_ANALYSIS_OP_TYPE_LEA;
	// psh / pop a single-data-memory operand onto / off the stack.
	case TMS320C55_INS_PSH: return RZ_ANALYSIS_OP_TYPE_PUSH;
	case TMS320C55_INS_POP: return RZ_ANALYSIS_OP_TYPE_POP;
	case TMS320C55_INS_PSHBOTH: return RZ_ANALYSIS_OP_TYPE_PUSH;
	case TMS320C55_INS_POPBOTH: return RZ_ANALYSIS_OP_TYPE_POP;
	case TMS320C55_INS_ADD: return RZ_ANALYSIS_OP_TYPE_ADD;
	case TMS320C55_INS_SUB: return RZ_ANALYSIS_OP_TYPE_SUB;
	// mpy Tx, [ACy,] ACx: a register multiply (ACx = Tx * ACy(32-16)).
	case TMS320C55_INS_MPY: return RZ_ANALYSIS_OP_TYPE_MUL;
	case TMS320C55_INS_MPYK: return RZ_ANALYSIS_OP_TYPE_MUL;
	case TMS320C55_INS_MACK: return RZ_ANALYSIS_OP_TYPE_MUL;
	// mpym Smem, Cmem, ACx: a memory multiply (ACx = Smem * Cmem).
	case TMS320C55_INS_MPYM: return RZ_ANALYSIS_OP_TYPE_MUL;
	// macm / masm Smem, Cmem, ACx: memory multiply-accumulate / -subtract.
	case TMS320C55_INS_MACM: return RZ_ANALYSIS_OP_TYPE_MUL;
	case TMS320C55_INS_MASM: return RZ_ANALYSIS_OP_TYPE_MUL;
	// mac / mas Smem, uns(Cmem), ACx: the unsigned-coefficient memory MACs.
	case TMS320C55_INS_MAC: return RZ_ANALYSIS_OP_TYPE_MUL;
	case TMS320C55_INS_MAS: return RZ_ANALYSIS_OP_TYPE_MUL;
	// sqrm / sqam / sqsm Smem, [ACx,] ACy: the squaring multiplies (ACy =
	// [ACx +/-] Smem * Smem). The legacy decoder leaves these untyped and
	// unlifted; the shared path classifies them as multiplies and lifts them.
	case TMS320C55_INS_SQRM: return RZ_ANALYSIS_OP_TYPE_MUL;
	case TMS320C55_INS_SQAM: return RZ_ANALYSIS_OP_TYPE_MUL;
	case TMS320C55_INS_SQSM: return RZ_ANALYSIS_OP_TYPE_MUL;
	// The 0x54 register square / square-accumulate forms are genuine multiplies
	// (ACy = ACx*ACx [+/- ACy]); the legacy left them untyped (null).
	case TMS320C55_INS_SQR: return RZ_ANALYSIS_OP_TYPE_MUL;
	case TMS320C55_INS_SQA: return RZ_ANALYSIS_OP_TYPE_MUL;
	case TMS320C55_INS_SQS: return RZ_ANALYSIS_OP_TYPE_MUL;
	// addv / addrv (addition with absolute value) -- typed as an addition.
	case TMS320C55_INS_ADDV: return RZ_ANALYSIS_OP_TYPE_ADD;
	case TMS320C55_INS_ADDRV: return RZ_ANALYSIS_OP_TYPE_ADD;
	// aadd #k8, sp (address-arithmetic add, frame setup) -- an addition.
	case TMS320C55_INS_AADD: return RZ_ANALYSIS_OP_TYPE_ADD;
	// amov #k16, dst: legacy types the constant/address load as LEA; the actual
	// dst = zero-extend(#k16) semantics come from the C55_LOP_AMOV lifter.
	case TMS320C55_INS_AMOV: return RZ_ANALYSIS_OP_TYPE_LEA;
	// rptb / rptcc: block / conditional repeat loop control.
	case TMS320C55_INS_RPTB:
	case TMS320C55_INS_RPTCC: return RZ_ANALYSIS_OP_TYPE_REP;
	// rptadd / rptsub: adjust the single-repeat counter and repeat.
	case TMS320C55_INS_RPTADD:
	case TMS320C55_INS_RPTSUB: return RZ_ANALYSIS_OP_TYPE_REP;
	// cmp / cmpand / cmpor SRC <relop> DST, TCz: register compare writing a TC bit.
	case TMS320C55_INS_CMP:
	case TMS320C55_INS_CMPAND:
	case TMS320C55_INS_CMPOR: return RZ_ANALYSIS_OP_TYPE_CMP;
	// swap / swapp / swap4: register (pair) exchange.
	case TMS320C55_INS_SWAP: return RZ_ANALYSIS_OP_TYPE_XCHG;
	case TMS320C55_INS_SWAPP: return RZ_ANALYSIS_OP_TYPE_XCHG;
	case TMS320C55_INS_SWAP4: return RZ_ANALYSIS_OP_TYPE_XCHG;
	// sftl / sfts / sftsc ACx, Tx[, ACy]: a register shift by a T-register count
	// (the sign of Tx selects the direction). Typed as a shift like the legacy.
	case TMS320C55_INS_SFTL: return RZ_ANALYSIS_OP_TYPE_SHL;
	case TMS320C55_INS_SFTS: return RZ_ANALYSIS_OP_TYPE_SHL;
	case TMS320C55_INS_SFTSC: return RZ_ANALYSIS_OP_TYPE_SHL;
	// firsadd / firssub Xmem, Ymem, Cmem, ACx, ACy: a FIR-filter step combining a
	// multiply-accumulate (ACy += ACx.h * Cmem) with a shifted (anti)symmetric sum
	// (ACx = (Xmem<<16) +/- (Ymem<<16)). The legacy left these unlifted with an
	// inconsistent type (firsadd -> lea, firssub -> null); the shared path lifts
	// them and classifies both as the multiply that dominates.
	case TMS320C55_INS_FIRSADD: return RZ_ANALYSIS_OP_TYPE_MUL;
	case TMS320C55_INS_FIRSSUB: return RZ_ANALYSIS_OP_TYPE_MUL;
	// sqdst squares the ACx high word into ACy (a multiply); abdst only adds an
	// absolute value, so it keeps the legacy's null type (no precise rizin type).
	case TMS320C55_INS_SQDST: return RZ_ANALYSIS_OP_TYPE_MUL;
	// lms multiply-accumulates Xmem*Ymem into ACy (with a parallel shifted add into
	// ACx); the legacy left it unlifted and null-typed -- classify as the multiply.
	case TMS320C55_INS_LMS: return RZ_ANALYSIS_OP_TYPE_MUL;
	// neg has no dedicated RzAnalysis op type; the legacy analysis reports it
	// as a subtraction, and the lifter distinguishes it via C55_LOP_NEG.
	case TMS320C55_INS_NEG: return RZ_ANALYSIS_OP_TYPE_SUB;
	// max / min likewise have no dedicated op type; the legacy analysis reports
	// them as compares, and the lifter selects them via C55_LOP_MAX / _MIN.
	case TMS320C55_INS_MAX: return RZ_ANALYSIS_OP_TYPE_CMP;
	case TMS320C55_INS_MIN: return RZ_ANALYSIS_OP_TYPE_CMP;
	case TMS320C55_INS_AND: return RZ_ANALYSIS_OP_TYPE_AND;
	case TMS320C55_INS_BTST: return RZ_ANALYSIS_OP_TYPE_AND;
	case TMS320C55_INS_BTSTSET: return RZ_ANALYSIS_OP_TYPE_AND;
	case TMS320C55_INS_BAND: return RZ_ANALYSIS_OP_TYPE_AND;
	case TMS320C55_INS_BTSTCLR: return RZ_ANALYSIS_OP_TYPE_AND;
	case TMS320C55_INS_BTSTNOT: return RZ_ANALYSIS_OP_TYPE_AND;
	case TMS320C55_INS_BSET: return RZ_ANALYSIS_OP_TYPE_MOV;
	case TMS320C55_INS_BCLR: return RZ_ANALYSIS_OP_TYPE_MOV;
	// bfxtr K16, ACx, ACy: extract the bits of ACx selected by the K16 mask and
	// right-pack them into ACy -- a register field move (the companion bfxpa
	// stays untyped, as the legacy decoder left it).
	case TMS320C55_INS_BFXTR: return RZ_ANALYSIS_OP_TYPE_MOV;
	case TMS320C55_INS_BNOT: return RZ_ANALYSIS_OP_TYPE_XOR;
	case TMS320C55_INS_ADDSUBCC: return RZ_ANALYSIS_OP_TYPE_ADD;
	case TMS320C55_INS_SUBC: return RZ_ANALYSIS_OP_TYPE_SUB;
	case TMS320C55_INS_ADDSUB: return RZ_ANALYSIS_OP_TYPE_ADD;
	case TMS320C55_INS_SUBADD: return RZ_ANALYSIS_OP_TYPE_SUB;
	case TMS320C55_INS_OR: return RZ_ANALYSIS_OP_TYPE_OR;
	case TMS320C55_INS_XOR: return RZ_ANALYSIS_OP_TYPE_XOR;
	case TMS320C55_INS_NOT: return RZ_ANALYSIS_OP_TYPE_NOT;
	// Register-indirect branch / call (b acx / call acx) transfer control to an
	// address held in an accumulator, so they are unconditional indirect forms.
	case TMS320C55_INS_B: return RZ_ANALYSIS_OP_TYPE_UJMP;
	case TMS320C55_INS_CALL: return RZ_ANALYSIS_OP_TYPE_UCALL;
	// reset triggers a non-maskable software reset; model it as a trap.
	case TMS320C55_INS_RESET: return RZ_ANALYSIS_OP_TYPE_TRAP;
	// intr / trap raise a software interrupt / trap to a vector number.
	case TMS320C55_INS_INTR: return RZ_ANALYSIS_OP_TYPE_SWI;
	case TMS320C55_INS_TRAP: return RZ_ANALYSIS_OP_TYPE_TRAP;
	// rpt sets up a single-instruction hardware repeat (a loop construct).
	case TMS320C55_INS_RPT: return RZ_ANALYSIS_OP_TYPE_REP;
	// rptblocal arms a local block-repeat; likewise a loop construct.
	case TMS320C55_INS_RPTBLOCAL: return RZ_ANALYSIS_OP_TYPE_REP;
	// ret / reti pop the return (or interrupt-return) address; both are returns.
	case TMS320C55_INS_RET: return RZ_ANALYSIS_OP_TYPE_RET;
	case TMS320C55_INS_RETI: return RZ_ANALYSIS_OP_TYPE_RET;
	// xcc / xccpart predicate the following instruction(s) on a condition; model
	// them as a compare (matching the legacy type).
	case TMS320C55_INS_XCC: return RZ_ANALYSIS_OP_TYPE_CMP;
	case TMS320C55_INS_XCCPART: return RZ_ANALYSIS_OP_TYPE_CMP;
	case TMS320C55_INS_BCC: return RZ_ANALYSIS_OP_TYPE_CJMP;
	case TMS320C55_INS_CALLCC: return RZ_ANALYSIS_OP_TYPE_CCALL;
	case TMS320C55_INS_RETCC: return RZ_ANALYSIS_OP_TYPE_CRET;
	default: return RZ_ANALYSIS_OP_TYPE_NULL;
	}
}

static ut8 c55x_insn_len(const ut8 *buf, int len) {
	const int sz = c55x_op_size(buf, len);
	return (sz > 0) ? (ut8)sz : 0;
}

// --- dual "::" MAC operand filling ---------------------------------------
// One Xmem / Ymem operand of a dual MAC: a 3-bit ARn selector and a 3-bit
// addressing mode. Unlike the single-data Smem 4-bit mode, the dual modes are
// 0:*ARn 1:*ARn+ 2:*ARn- 3:*(ARn+T0) 4:*(ARn+T1) 5:*(ARn-T0) 6:*(ARn-T1)
// 7:*ARn(T0); the uns() wrapper (zero-extend on load) is shared with this
// sub-MAC's Cmem coefficient.
static void c55x_dual_mem(C55Operand *out, ut8 ar, ut8 mode, bool uns) {
	memset(out, 0, sizeof(*out));
	out->kind = C55_OP_MEM;
	out->access = 16;
	out->uns = uns;
	c55x_gr4((ut8)(8 + (ar & 7)), &out->reg); // base ARn
	switch (mode & 7) {
	case 0: out->amode = C55_AM_INDIRECT; break;
	case 1: out->amode = C55_AM_POSTINC; break;
	case 2: out->amode = C55_AM_POSTDEC; break;
	case 3:
		out->amode = C55_AM_POSTADD;
		c55x_gr4(4, &out->index);
		break; // *(ARn+T0)
	case 4:
		out->amode = C55_AM_POSTADD;
		c55x_gr4(5, &out->index);
		break; // *(ARn+T1)
	case 5:
		out->amode = C55_AM_POSTSUB;
		c55x_gr4(4, &out->index);
		break; // *(ARn-T0)
	case 6:
		out->amode = C55_AM_POSTSUB;
		c55x_gr4(5, &out->index);
		break; // *(ARn-T1)
	default:
		out->amode = C55_AM_IDXREG;
		c55x_gr4(4, &out->index);
		break; // *ARn(T0)
	}
}

// The shared Cmem coefficient (*CDP with the same post-modify mm as the single
// MACs); each sub-MAC reads it with its own uns() signedness.
static void c55x_dual_cmem(C55Operand *out, ut8 cmode, bool uns) {
	memset(out, 0, sizeof(*out));
	out->kind = C55_OP_MEM;
	out->reg.cls = C55_RC_CDP;
	out->access = 16;
	out->uns = uns;
	switch (cmode & 3) {
	case 0: out->amode = C55_AM_INDIRECT; break;
	case 1: out->amode = C55_AM_POSTINC; break;
	case 2: out->amode = C55_AM_POSTDEC; break;
	default: // *(CDP+T0)
		out->amode = C55_AM_POSTADD;
		out->index.cls = C55_RC_T;
		out->index.num = 0;
		break;
	}
}

static void c55x_dual_ac(const C55ArchDesc *a, C55Operand *out, ut8 num) {
	memset(out, 0, sizeof(*out));
	out->kind = C55_OP_REG;
	out->reg.cls = C55_RC_AC;
	out->reg.num = (ut8)(num & 3);
	const C55RegInfo *ri = a->reg_info ? a->reg_info(out->reg.cls, out->reg.num, C55_SUB_NONE) : NULL;
	out->width = ri ? ri->width : 0;
}

// Fill the canonical dual-MAC operand layout from the 4-byte word. Byte1 carries
// the Xmem ARn (bits 5-7) and mode (bits 2-4) and the Ymem ARn low bits (0-1);
// byte2 the Cmem mode (bits 0-1), the op selector (bits 2-3, already matched),
// the Ymem mode (bits 4-6) and the Ymem ARn high bit (bit 7); byte3 the round R
// (bit 0), the 40-bit flag (bit 1), ACx (bits 2-3), ACy (bits 4-5) and the per-
// sub uns flags (bit 6 = sub2/Ymem, bit 7 = sub1/Xmem). An amar sub1 has no Cmem
// or destination, and the single accumulator field then names sub2's destination.
static bool c55x_fill_dual(const C55ArchDesc *a, ut64 bits, const C55InsnDef *def, C55Insn *out) {
	const ut8 b1 = (ut8)((bits >> 16) & 0xff);
	const ut8 b2 = (ut8)((bits >> 8) & 0xff);
	const ut8 b3 = (ut8)(bits & 0xff);
	out->dual = true;
	out->lop2 = def->lop2;
	out->amar1 = def->amar1;
	out->shift1 = def->shift1;
	out->shift2 = def->shift2;
	out->round = b3 & 1;
	out->m40 = (b3 >> 1) & 1;
	const bool uns_y = (b3 >> 6) & 1;
	const bool uns_x = (b3 >> 7) & 1;
	const ut8 acx = (ut8)((b3 >> 2) & 3);
	const ut8 acy = (ut8)((b3 >> 4) & 3);
	c55x_dual_mem(&out->ops[0], (ut8)((b1 >> 5) & 7), (ut8)((b1 >> 2) & 7), uns_x); // Xmem
	c55x_dual_cmem(&out->ops[1], (ut8)(b2 & 3), uns_x); // Cmem (sub1)
	c55x_dual_ac(a, &out->ops[2], acx); // ACx
	c55x_dual_mem(&out->ops[3], (ut8)(((b1 & 3) << 1) | ((b2 >> 7) & 1)), (ut8)((b2 >> 4) & 7), uns_y); // Ymem
	c55x_dual_cmem(&out->ops[4], (ut8)(b2 & 3), uns_y); // Cmem (sub2)
	c55x_dual_ac(a, &out->ops[5], acy); // ACy
	if (def->amar1) {
		// amar sub1 has no Cmem / destination and renders no uns() wrapper; the
		// single accumulator field (ACx slot) is sub2's destination, and sub2's
		// uns comes from the high uu bit (uns_x); the low bit is unused here.
		out->ops[0].uns = false;
		out->ops[1].kind = C55_OP_NONE;
		out->ops[2].kind = C55_OP_NONE;
		out->ops[3].uns = uns_x;
		out->ops[4].uns = uns_x;
		c55x_dual_ac(a, &out->ops[5], acx); // single dst -> sub2
	}
	out->n_ops = 6;
	return true;
}

// Operand extractors for the triple-register amar (amar Xmem, Ymem, Cmem):
// the same Xmem / Ymem / Cmem fields as the dual MACs, but as three plain
// address-modify operands (no uns, no destination).
static void c55x_x_dual_xmem(const C55ArchDesc *a, ut64 bits, const C55OpDesc *d, C55Operand *out) {
	(void)a;
	(void)d;
	const ut8 b1 = (ut8)((bits >> 16) & 0xff);
	c55x_dual_mem(out, (ut8)((b1 >> 5) & 7), (ut8)((b1 >> 2) & 7), false);
}
static void c55x_x_dual_ymem(const C55ArchDesc *a, ut64 bits, const C55OpDesc *d, C55Operand *out) {
	(void)a;
	(void)d;
	const ut8 b1 = (ut8)((bits >> 16) & 0xff);
	const ut8 b2 = (ut8)((bits >> 8) & 0xff);
	c55x_dual_mem(out, (ut8)(((b1 & 3) << 1) | ((b2 >> 7) & 1)), (ut8)((b2 >> 4) & 7), false);
}
static void c55x_x_dual_cmem3(const C55ArchDesc *a, ut64 bits, const C55OpDesc *d, C55Operand *out) {
	(void)a;
	(void)d;
	c55x_dual_cmem(out, (ut8)((bits >> 8) & 3), false);
}

// Dual-memory Xmem/Ymem fields for the 3-byte mov / add / sub Xmem,Ymem forms
// (opcodes 0x80 / 0x81). Unlike the 4-byte 0x82-0x86 MAC family (where the
// fields sit in byte1/byte2), here byte1 = XXXMMMYY and byte2 = YMMM00xx, one
// byte earlier in the packed word -- so read byte1 = bits[15:8], byte2 =
// bits[7:0]. Xmem: ARn = byte1[7:5], mode = byte1[4:2]. Ymem: ARn =
// (byte1[1:0]<<1)|byte2[7], mode = byte2[6:4].
static void c55x_x_dual_xmem3(const C55ArchDesc *a, ut64 bits, const C55OpDesc *d, C55Operand *out) {
	(void)a;
	(void)d;
	const ut8 b1 = (ut8)((bits >> 8) & 0xff);
	c55x_dual_mem(out, (ut8)((b1 >> 5) & 7), (ut8)((b1 >> 2) & 7), false);
}
static void c55x_x_dual_ymem3(const C55ArchDesc *a, ut64 bits, const C55OpDesc *d, C55Operand *out) {
	(void)a;
	(void)d;
	const ut8 b1 = (ut8)((bits >> 8) & 0xff);
	const ut8 b2 = (ut8)(bits & 0xff);
	c55x_dual_mem(out, (ut8)(((b1 & 3) << 1) | ((b2 >> 7) & 1)), (ut8)((b2 >> 4) & 7), false);
}
// dbl() variants: the "mov dbl(Xmem), dbl(Ymem)" form moves a 32-bit long word.
static void c55x_x_dual_xmem3_dbl(const C55ArchDesc *a, ut64 bits, const C55OpDesc *d, C55Operand *out) {
	c55x_x_dual_xmem3(a, bits, d, out);
	if (out->kind == C55_OP_MEM) {
		out->dbl = true;
		out->access = 32;
	}
}
static void c55x_x_dual_ymem3_dbl(const C55ArchDesc *a, ut64 bits, const C55OpDesc *d, C55Operand *out) {
	c55x_x_dual_ymem3(a, bits, d, out);
	if (out->kind == C55_OP_MEM) {
		out->dbl = true;
		out->access = 32;
	}
}

// 0x86 dual-multiply (mpym / macm / masm Xmem, Ymem, ...): Xmem and Ymem share
// the 0x82-0x85 byte1/byte2 addressing-field layout, but the per-operand uns()
// qualifiers live in the last byte (bit 3 = Xmem, bit 2 = Ymem) and the source /
// destination accumulators in byte 2 (DD = bits 0-1, SS = bits 2-3).
static void c55x_x_xymac_xmem(const C55ArchDesc *a, ut64 bits, const C55OpDesc *d, C55Operand *out) {
	(void)a;
	(void)d;
	const ut8 b1 = (ut8)((bits >> 16) & 0xff);
	c55x_dual_mem(out, (ut8)((b1 >> 5) & 7), (ut8)((b1 >> 2) & 7), (bool)((bits >> 3) & 1));
}
static void c55x_x_xymac_ymem(const C55ArchDesc *a, ut64 bits, const C55OpDesc *d, C55Operand *out) {
	(void)a;
	(void)d;
	const ut8 b1 = (ut8)((bits >> 16) & 0xff);
	const ut8 b2 = (ut8)((bits >> 8) & 0xff);
	c55x_dual_mem(out, (ut8)(((b1 & 3) << 1) | ((b2 >> 7) & 1)), (ut8)((b2 >> 4) & 7), (bool)((bits >> 2) & 1));
}
// Source accumulator of the dual-multiply MACs (macm / masm Xmem, Ymem, ACx, ACy):
// SS is byte2 bits 2-3, DD byte2 bits 0-1. When the two coincide the instruction
// is the two-operand form (ACy += Xmem*Ymem) and the explicit source slot is
// dropped (the generic ops[] loop compacts the NONE slot away).
static void c55x_x_xymac_src(const C55ArchDesc *a, ut64 bits, const C55OpDesc *d, C55Operand *out) {
	(void)d;
	ut8 dd = (ut8)((bits >> 8) & 3);
	ut8 ss = (ut8)((bits >> 10) & 3);
	if (ss == dd) {
		out->kind = C55_OP_NONE;
		return;
	}
	c55x_dual_ac(a, out, ss);
}

static const C55InsnDef c55x_table[] = {
	// nop: single-byte 0x20 (shared encoding with C55x+).
	{ .mask = 0xfe000000, .match = 0x20000000, .id = TMS320C55_INS_NOP },
	// mov src, dst (opcode 0x22): the operand byte's high nibble selects the
	// source and the low nibble the destination, each a 4-bit register field
	// (AC0-3 / T0-3 / AR0-7). ops[0]=src, ops[1]=dst per the shared lifter.
	{ .mask = 0xfe000000, .match = 0x22000000, .id = TMS320C55_INS_MOV, .len = 2, .ops = { { .lo = 4, .width = 4, .fn = c55x_x_gr4 }, { .lo = 0, .width = 4, .fn = c55x_x_gr4 } } },
	// add / sub src, dst (opcodes 0x24 / 0x26): same operand byte as mov (high
	// nibble source, low nibble destination). dst = dst <op> src; the
	// destination is elided when it equals the source (the legacy single-operand
	// "add ac0" form).
	{ .mask = 0xfe000000, .match = 0x24000000, .id = TMS320C55_INS_ADD, .lop = C55_LOP_AREG_ADD, .len = 2, .ops = { { .lo = 4, .width = 4, .fn = c55x_x_gr4 }, { .lo = 0, .width = 4, .fn = c55x_x_gr4_elide } } },
	{ .mask = 0xfe000000, .match = 0x26000000, .id = TMS320C55_INS_SUB, .lop = C55_LOP_AREG_SUB, .len = 2, .ops = { { .lo = 4, .width = 4, .fn = c55x_x_gr4 }, { .lo = 0, .width = 4, .fn = c55x_x_gr4_elide } } },
	// and / or / xor src, dst (opcodes 0x28 / 0x2a / 0x2c): same operand byte
	// as mov (high nibble source, low nibble destination). dst = dst <op> src
	// via the shared lifter (equal-width forms; mixed-width forms fall back).
	{ .mask = 0xfe000000, .match = 0x28000000, .id = TMS320C55_INS_AND, .lop = C55_LOP_AREG_AND, .len = 2, .ops = { { .lo = 4, .width = 4, .fn = c55x_x_gr4 }, { .lo = 0, .width = 4, .fn = c55x_x_gr4 } } },
	{ .mask = 0xfe000000, .match = 0x2a000000, .id = TMS320C55_INS_OR, .lop = C55_LOP_AREG_OR, .len = 2, .ops = { { .lo = 4, .width = 4, .fn = c55x_x_gr4 }, { .lo = 0, .width = 4, .fn = c55x_x_gr4 } } },
	{ .mask = 0xfe000000, .match = 0x2c000000, .id = TMS320C55_INS_XOR, .lop = C55_LOP_AREG_XOR, .len = 2, .ops = { { .lo = 4, .width = 4, .fn = c55x_x_gr4 }, { .lo = 0, .width = 4, .fn = c55x_x_gr4 } } },
	// not src[, dst] (opcode 0x36): dst = ~src. The source is optional in the
	// short form; the destination collapses against it (elided when equal).
	{ .mask = 0xfe000000, .match = 0x36000000, .id = TMS320C55_INS_NOT, .len = 2, .ops = { { .lo = 4, .width = 4, .fn = c55x_x_gr4 }, { .lo = 0, .width = 4, .fn = c55x_x_gr4_elide } } },
	// neg src[, dst] (opcode 0x34): dst = -src. Optional-source short form; the
	// destination collapses against the source (elided when equal). The lifter
	// uses C55_LOP_NEG since neg shares the SUB op type.
	{ .mask = 0xfe000000, .match = 0x34000000, .id = TMS320C55_INS_NEG, .lop = C55_LOP_NEG, .len = 2, .ops = { { .lo = 4, .width = 4, .fn = c55x_x_gr4 }, { .lo = 0, .width = 4, .fn = c55x_x_gr4_elide } } },
	// max / min src[, dst] (opcodes 0x2e / 0x30): dst = max/min(src, dst).
	// Optional-source short form (destination elided when equal to the source);
	// the lifter emits the ite via C55_LOP_MAX / _MIN since both share CMP.
	{ .mask = 0xfe000000, .match = 0x2e000000, .id = TMS320C55_INS_MAX, .lop = C55_LOP_MAX, .len = 2, .ops = { { .lo = 4, .width = 4, .fn = c55x_x_gr4 }, { .lo = 0, .width = 4, .fn = c55x_x_gr4_elide } } },
	{ .mask = 0xfe000000, .match = 0x30000000, .id = TMS320C55_INS_MIN, .lop = C55_LOP_MIN, .len = 2, .ops = { { .lo = 4, .width = 4, .fn = c55x_x_gr4 }, { .lo = 0, .width = 4, .fn = c55x_x_gr4_elide } } },
	// abs src[, dst] (opcode 0x32): dst = |src|. Optional-source short form (the
	// destination is elided when equal to the source). abs has no RzAnalysis op
	// type at all (the legacy analysis reports type null), so it reaches the
	// shared path via its lift-op rather than its op type; the lifter emits the
	// ite through C55_LOP_ABS.
	{ .mask = 0xfe000000, .match = 0x32000000, .id = TMS320C55_INS_ABS, .lop = C55_LOP_ABS, .len = 2, .ops = { { .lo = 4, .width = 4, .fn = c55x_x_gr4 }, { .lo = 0, .width = 4, .fn = c55x_x_gr4_elide } } },
	// mov k4, dst (opcode 0x3c): 4-bit unsigned immediate (operand byte's high
	// nibble) into the register selected by the low nibble. dst = k4, the
	// immediate zero-extended to the destination width by the shared lifter.
	{ .mask = 0xfe000000, .match = 0x3c000000, .id = TMS320C55_INS_MOV, .len = 2, .ops = { { .lo = 4, .width = 4, .fn = c55_x_imm }, { .lo = 0, .width = 4, .fn = c55x_x_gr4 } } },
	// mov -#k, dst (opcode 0x3e): like mov k4 but the high nibble is a negated
	// magnitude (-0 .. -15). The legacy leaves it unlifted, so the shared MOV
	// lifter declines it (see c55x_x_negk4 / the neg_imm guard).
	{ .mask = 0xfe000000, .match = 0x3e000000, .id = TMS320C55_INS_MOV, .len = 2, .ops = { { .lo = 4, .width = 4, .fn = c55x_x_negk4 }, { .lo = 0, .width = 4, .fn = c55x_x_gr4 } } },
	// mov #k12, <reg> (opcode 0x16): a 12-bit immediate (bytes 1-2, bits 12-23)
	// into a control register selected by byte2's low nibble (dph/pdp/bk03/bk47/
	// bkc/csr/brc0/brc1). The immediate is masked to the register width; the
	// shared MOV lifter emits "reg = k".
	{ .mask = 0xfe000000, .match = 0x16000000, .id = TMS320C55_INS_MOV, .len = 3, .ops = { { .lo = 4, .width = 12, .fn = c55x_x_16imm }, { .lo = 0, .width = 4, .fn = c55x_x_16dst } } },
	// mov #k16, <reg> (opcode 0x78): a 16-bit immediate (instruction bytes 1-2)
	// into a control register selected by byte3 (see c55x_x_78dst). dst = k16,
	// the immediate zero-extended to the dst width by the shared MOV lifter.
	{ .mask = 0xff000000, .match = 0x78000000, .id = TMS320C55_INS_MOV, .len = 4, .no_parallel = true, .ops = { { .lo = 8, .width = 16, .fn = c55_x_imm }, { .lo = 1, .width = 4, .fn = c55x_x_78dst } } },
	// ACy = ACx <op> (#k16 << #sh) (opcodes 0x70 add, 0x71 sub, 0x72 and,
	// 0x73 or, 0x74 xor): a 16-bit immediate (bytes 1-2) shifted up by byte3
	// bits 0-3 (0-15), combined with ACx into ACy. byte3 bits 6-7 select ACx,
	// bits 4-5 ACy (elided when equal to ACx). The 0x75 mov form is left to the
	// legacy decoder (it carries no IL there).
	{ .mask = 0xff000000, .match = 0x70000000, .id = TMS320C55_INS_ADD, .lop = C55_LOP_ADDSHL, .len = 4, .no_parallel = true, .ops = { { .lo = 8, .width = 16, .fn = c55x_x_imm_varsh }, { .lo = 6, .fn = c55x_x_ac2 }, { .lo = 4, .fn = c55x_x_ac2_elide } } },
	{ .mask = 0xff000000, .match = 0x71000000, .id = TMS320C55_INS_SUB, .lop = C55_LOP_SUBSHL, .len = 4, .no_parallel = true, .ops = { { .lo = 8, .width = 16, .fn = c55x_x_imm_varsh }, { .lo = 6, .fn = c55x_x_ac2 }, { .lo = 4, .fn = c55x_x_ac2_elide } } },
	{ .mask = 0xff000000, .match = 0x72000000, .id = TMS320C55_INS_AND, .lop = C55_LOP_ANDSHL, .len = 4, .no_parallel = true, .ops = { { .lo = 8, .width = 16, .fn = c55x_x_imm_varsh }, { .lo = 6, .fn = c55x_x_ac2 }, { .lo = 4, .fn = c55x_x_ac2_elide } } },
	{ .mask = 0xff000000, .match = 0x73000000, .id = TMS320C55_INS_OR, .lop = C55_LOP_ORSHL, .len = 4, .no_parallel = true, .ops = { { .lo = 8, .width = 16, .fn = c55x_x_imm_varsh }, { .lo = 6, .fn = c55x_x_ac2 }, { .lo = 4, .fn = c55x_x_ac2_elide } } },
	{ .mask = 0xff000000, .match = 0x74000000, .id = TMS320C55_INS_XOR, .lop = C55_LOP_XORSHL, .len = 4, .no_parallel = true, .ops = { { .lo = 8, .width = 16, .fn = c55x_x_imm_varsh }, { .lo = 6, .fn = c55x_x_ac2 }, { .lo = 4, .fn = c55x_x_ac2_elide } } },
	// mov #k16 << #sh, ACy (opcode 0x75): the immediate shifted into ACy; no
	// source accumulator and (matching the legacy) no IL -- the MOV lifter
	// declines the variable-shift form, leaving it unlifted.
	{ .mask = 0xff000000, .match = 0x75000000, .id = TMS320C55_INS_MOV, .len = 4, .no_parallel = true, .ops = { { .lo = 8, .width = 16, .fn = c55x_x_imm_varsh }, { .lo = 4, .fn = c55x_x_ac2 } } },
	// ACy = ACx <op> (#k16 << #16) (opcode 0x7a): a 16-bit immediate (bytes 1-2)
	// shifted up 16 bits, combined with ACx into ACy. byte3 bits 1-3 select the
	// operation (0 add, 1 sub, 2 and, 3 or, 4 xor; 5 mov / 6 idle / 7 invalid
	// are left to the legacy decoder), bits 6-7 ACx, bits 4-5 ACy (elided when
	// equal to ACx); byte3 bit 0 is don't-care.
	{ .mask = 0xff00000e, .match = 0x7a000000, .id = TMS320C55_INS_ADD, .lop = C55_LOP_ADDSHL, .len = 4, .no_parallel = true, .ops = { { .lo = 8, .width = 16, .fn = c55x_x_imm_sh16 }, { .lo = 6, .fn = c55x_x_ac2 }, { .lo = 4, .fn = c55x_x_ac2_elide } } },
	{ .mask = 0xff00000e, .match = 0x7a000002, .id = TMS320C55_INS_SUB, .lop = C55_LOP_SUBSHL, .len = 4, .no_parallel = true, .ops = { { .lo = 8, .width = 16, .fn = c55x_x_imm_sh16 }, { .lo = 6, .fn = c55x_x_ac2 }, { .lo = 4, .fn = c55x_x_ac2_elide } } },
	{ .mask = 0xff00000e, .match = 0x7a000004, .id = TMS320C55_INS_AND, .lop = C55_LOP_ANDSHL, .len = 4, .no_parallel = true, .ops = { { .lo = 8, .width = 16, .fn = c55x_x_imm_sh16 }, { .lo = 6, .fn = c55x_x_ac2 }, { .lo = 4, .fn = c55x_x_ac2_elide } } },
	{ .mask = 0xff00000e, .match = 0x7a000006, .id = TMS320C55_INS_OR, .lop = C55_LOP_ORSHL, .len = 4, .no_parallel = true, .ops = { { .lo = 8, .width = 16, .fn = c55x_x_imm_sh16 }, { .lo = 6, .fn = c55x_x_ac2 }, { .lo = 4, .fn = c55x_x_ac2_elide } } },
	{ .mask = 0xff00000e, .match = 0x7a000008, .id = TMS320C55_INS_XOR, .lop = C55_LOP_XORSHL, .len = 4, .no_parallel = true, .ops = { { .lo = 8, .width = 16, .fn = c55x_x_imm_sh16 }, { .lo = 6, .fn = c55x_x_ac2 }, { .lo = 4, .fn = c55x_x_ac2_elide } } },
	// mov #k16 << #16, ACy (opcode 0x7a, byte3 selector 5): the immediate (sign-
	// extended, shifted up 16) loaded into ACy; there is no source accumulator.
	{ .mask = 0xff00000e, .match = 0x7a00000a, .id = TMS320C55_INS_MOV, .len = 4, .no_parallel = true, .ops = { { .lo = 8, .width = 16, .fn = c55x_x_imm_sh16 }, { .lo = 4, .fn = c55x_x_ac2 } } },
	// dst = ACx <op> zero-extend(#k16) (opcodes 0x7b add, 0x7c sub, 0x7d and,
	// 0x7e or, 0x7f xor): a 16-bit immediate (bytes 1-2), the source accumulator
	// in byte3 bits 0-3 (gr4) and the destination in bits 4-7 (gr4). add / sub
	// collapse the destination when it equals the source ("add #k, ACx"); the
	// bitwise forms always render all three operands. The lifter handles an
	// accumulator source and (matching the legacy) leaves a T / AR source
	// unlifted; a 16-bit destination truncates the 40-bit result.
	{ .mask = 0xff000000, .match = 0x7b000000, .id = TMS320C55_INS_ADD, .lop = C55_LOP_ADDK, .len = 4, .no_parallel = true, .ops = { { .lo = 8, .width = 16, .fn = c55_x_imm }, { .lo = 0, .width = 4, .fn = c55x_x_gr4 }, { .lo = 4, .width = 4, .fn = c55x_x_gr4_elide } } },
	{ .mask = 0xff000000, .match = 0x7c000000, .id = TMS320C55_INS_SUB, .lop = C55_LOP_SUBK, .len = 4, .no_parallel = true, .ops = { { .lo = 8, .width = 16, .fn = c55_x_imm }, { .lo = 0, .width = 4, .fn = c55x_x_gr4 }, { .lo = 4, .width = 4, .fn = c55x_x_gr4_elide } } },
	{ .mask = 0xff000000, .match = 0x7d000000, .id = TMS320C55_INS_AND, .lop = C55_LOP_ANDK, .len = 4, .no_parallel = true, .ops = { { .lo = 8, .width = 16, .fn = c55_x_imm }, { .lo = 0, .width = 4, .fn = c55x_x_gr4 }, { .lo = 4, .width = 4, .fn = c55x_x_gr4 } } },
	{ .mask = 0xff000000, .match = 0x7e000000, .id = TMS320C55_INS_OR, .lop = C55_LOP_ORK, .len = 4, .no_parallel = true, .ops = { { .lo = 8, .width = 16, .fn = c55_x_imm }, { .lo = 0, .width = 4, .fn = c55x_x_gr4 }, { .lo = 4, .width = 4, .fn = c55x_x_gr4 } } },
	{ .mask = 0xff000000, .match = 0x7f000000, .id = TMS320C55_INS_XOR, .lop = C55_LOP_XORK, .len = 4, .no_parallel = true, .ops = { { .lo = 8, .width = 16, .fn = c55_x_imm }, { .lo = 0, .width = 4, .fn = c55x_x_gr4 }, { .lo = 4, .width = 4, .fn = c55x_x_gr4 } } },
	// dst = ACx <op> zero-extend(#k8) (opcodes 0x18 and, 0x1a or, 0x1c xor; the
	// parallel "|| " encodings 0x19 / 0x1b / 0x1d differ only in byte0 bit 0, so
	// the mask leaves that bit to the parallel-bit derivation): an 8-bit
	// immediate (byte1), the gr4 source in byte2 bits 0-3 and the gr4 destination
	// in bits 4-7. The same immediate-ALU lifter handles an accumulator source
	// and (matching the legacy) leaves a T / AR source unlifted; a 16-bit T / AR
	// destination truncates the result.
	{ .mask = 0xfe000000, .match = 0x18000000, .id = TMS320C55_INS_AND, .lop = C55_LOP_ANDK, .len = 3, .ops = { { .lo = 8, .width = 8, .fn = c55_x_imm }, { .lo = 0, .width = 4, .fn = c55x_x_gr4 }, { .lo = 4, .width = 4, .fn = c55x_x_gr4 } } },
	{ .mask = 0xfe000000, .match = 0x1a000000, .id = TMS320C55_INS_OR, .lop = C55_LOP_ORK, .len = 3, .ops = { { .lo = 8, .width = 8, .fn = c55_x_imm }, { .lo = 0, .width = 4, .fn = c55x_x_gr4 }, { .lo = 4, .width = 4, .fn = c55x_x_gr4 } } },
	{ .mask = 0xfe000000, .match = 0x1c000000, .id = TMS320C55_INS_XOR, .lop = C55_LOP_XORK, .len = 3, .ops = { { .lo = 8, .width = 8, .fn = c55_x_imm }, { .lo = 0, .width = 4, .fn = c55x_x_gr4 }, { .lo = 4, .width = 4, .fn = c55x_x_gr4 } } },
	// add / sub k4, dst (opcodes 0x40 / 0x42): same immediate layout as mov k4;
	// dst = dst +/- k4 with the immediate zero-extended to the dst width.
	{ .mask = 0xfe000000, .match = 0x40000000, .id = TMS320C55_INS_ADD, .len = 2, .ops = { { .lo = 4, .width = 4, .fn = c55_x_imm }, { .lo = 0, .width = 4, .fn = c55x_x_gr4 } } },
	{ .mask = 0xfe000000, .match = 0x42000000, .id = TMS320C55_INS_SUB, .len = 2, .ops = { { .lo = 4, .width = 4, .fn = c55_x_imm }, { .lo = 0, .width = 4, .fn = c55x_x_gr4 } } },
	// mov Smem, dst load (opcodes 0xa0-0xaf): the destination register is gr4
	// of the opcode's low nibble (AC0-3 / T0-3 / AR0-7) and the source is a
	// single data-memory operand. Only the register-modify addressing modes the
	// shared effective-address / memory primitives handle are decoded; the rest
	// fall back to the legacy decoder via c55x_x_smem. The low nibble is the
	// destination register, not the parallel marker, so the row opts out of the
	// parallel-bit derivation (no_parallel).
	{ .mask = 0xf0000000, .match = 0xa0000000, .id = TMS320C55_INS_MOV, .len = 2, .no_parallel = true, .ops = { { .lo = 0, .width = 8, .fn = c55x_x_smem }, { .lo = 8, .width = 4, .fn = c55x_x_gr4 } } },
	// mov reg, Smem store (opcodes 0xc0-0xcf): the direction mirror of the load.
	// The source register is gr4 of the opcode's low nibble and the destination
	// is a single data-memory operand; a wider accumulator source is truncated
	// to the 16-bit access by the shared memory primitives. Same addressing-mode
	// coverage and no_parallel rationale as the load row.
	{ .mask = 0xf0000000, .match = 0xc0000000, .id = TMS320C55_INS_MOV, .len = 2, .no_parallel = true, .ops = { { .lo = 8, .width = 4, .fn = c55x_x_gr4 }, { .lo = 0, .width = 8, .fn = c55x_x_smem } } },
	// mov hi(ACx), Smem store (opcodes 0xbc-0xbf): stores the high half of the
	// accumulator (ACx.h, accumulator number in the low two bits) to a single
	// data-memory operand. The shared half-source read produces the 16-bit high
	// half, stored through the same primitives. Same Smem coverage as the load
	// and store rows; the low two bits are the accumulator field, not the
	// parallel marker (no_parallel).
	{ .mask = 0xfc000000, .match = 0xbc000000, .id = TMS320C55_INS_MOV, .len = 2, .no_parallel = true, .ops = { { .lo = 8, .width = 2, .fn = c55x_x_achi }, { .lo = 0, .width = 8, .fn = c55x_x_smem } } },
	// mov Smem << #16, ACx (opcodes 0xb0-0xb3): loads a single data-memory
	// operand shifted left by 16 into an accumulator (number in the low two
	// bits). The shift is rendered but the load is not yet lifted (matching the
	// legacy decoder's empty IL); the low two bits are the accumulator field,
	// not the parallel marker (no_parallel).
	{ .mask = 0xfc000000, .match = 0xb0000000, .id = TMS320C55_INS_MOV, .len = 2, .no_parallel = true, .ops = { { .lo = 0, .width = 8, .fn = c55x_x_smem_sh16 }, { .lo = 8, .width = 2, .fn = c55x_x_ac2 } } },
	// amar Smem (opcode 0xb4): modify auxiliary register -- computes the
	// effective address and applies the addressing mode's post-modify side
	// effect without accessing memory. Single Smem operand (the low byte is the
	// addressing field, not the parallel marker -> no_parallel). It lifts to
	// that side effect, or a nop for non-modifying modes, and is typed LEA.
	{ .mask = 0xff000000, .match = 0xb4000000, .id = TMS320C55_INS_AMAR, .len = 2, .no_parallel = true, .ops = { { .lo = 0, .width = 8, .fn = c55x_x_smem } } },
	// amar Smem, xar (opcode 0xec): load the effective (word) address of the
	// single-data-memory operand into an extended pointer register, with no
	// memory access. byte1 is the Smem field; byte2's high nibble selects the
	// XARn destination (c55x_x_gr4a maps 8..15 -> xar0..7) and its low nibble
	// 0xe is the form marker (mask requires it, distinguishing this from the
	// other 0xec sub-opcodes). The shared amar lifter loads base+offset into
	// the destination for the SP/AR-relative modes; the *(k23) absolute sub-
	// form (Smem mode-8 base-1) is left invalid above and falls to the legacy
	// decoder, which sizes it correctly.
	{ .mask = 0xff000f00, .match = 0xec000e00, .id = TMS320C55_INS_AMAR, .len = 3, .no_parallel = true, .ops = { { .lo = 8, .width = 8, .fn = c55x_x_smem }, { .lo = 4, .width = 4, .fn = c55x_x_gr4a } } },
	// dbl(Lmem) 32-bit load / store / accumulate (opcodes 0xed load, 0xeb store).
	// These are 3-byte forms whose last byte (the packed value's low byte)
	// selects the sub-form: bits 3-1 == 100 is "mov dbl(Lmem), ACx" (DD = ACx in
	// bits 5-4); bits 3-1 == 000/001 are "add/sub dbl(Lmem), [ACx,] ACy" (DD = dst
	// ACy in bits 5-4, SS = optional src ACx in bits 7-6, suppressed when equal);
	// and bits 3-0 == 1111 is "mov dbl(Lmem), XAdst" (the extended pointer in
	// bits 7-4). The store opcode's "mov ACx, dbl(Lmem)" has bits 3,2,0 == 1,0,0
	// with SS = ACx in bits 5-4. The Lmem field is byte1; its *(k23) absolute
	// sub-mode supplies the true k24 address (the legacy decoder rendered it
	// wrongly). The dbl moves lift through the shared 32-bit memory mover; add/sub
	// fall back to the legacy lifter.
	{ .mask = 0xff000e00, .match = 0xed000800, .id = TMS320C55_INS_MOV, .len = 3, .no_parallel = true, .ops = { { .lo = 8, .width = 8, .fn = c55x_x_smem_dbl }, { .lo = 4, .width = 2, .param = C55_RC_AC, .fn = c55_x_reg } } },
	{ .mask = 0xff000f00, .match = 0xed000f00, .id = TMS320C55_INS_MOV, .len = 3, .no_parallel = true, .ops = { { .lo = 8, .width = 8, .fn = c55x_x_smem_dbl }, { .lo = 4, .width = 4, .fn = c55x_x_gr4a } } },
	{ .mask = 0xff000e00, .match = 0xed000000, .id = TMS320C55_INS_ADD, .len = 3, .no_parallel = true, .ops = { { .lo = 8, .width = 8, .fn = c55x_x_smem_dbl }, { .lo = 6, .width = 2, .fn = c55x_x_ac_src2 }, { .lo = 4, .width = 2, .param = C55_RC_AC, .fn = c55_x_reg } } },
	{ .mask = 0xff000e00, .match = 0xed000200, .id = TMS320C55_INS_SUB, .len = 3, .no_parallel = true, .ops = { { .lo = 8, .width = 8, .fn = c55x_x_smem_dbl }, { .lo = 6, .width = 2, .fn = c55x_x_ac_src2 }, { .lo = 4, .width = 2, .param = C55_RC_AC, .fn = c55_x_reg } } },
	{ .mask = 0xff000d00, .match = 0xeb000800, .id = TMS320C55_INS_MOV, .len = 3, .no_parallel = true, .ops = { { .lo = 4, .width = 2, .param = C55_RC_AC, .fn = c55_x_reg }, { .lo = 8, .width = 8, .fn = c55x_x_smem_dbl } } },
	// delay Smem (opcode 0xb6): memory delay -- copies the addressed word to
	// the next higher data address (TI SWPU104 6.7.1). Single Smem operand (the
	// low byte is the addressing field, so no_parallel; note 0xb7 is a distinct
	// opcode, psh dbl). Typed MOVE via c55x_op_type; the side effect is not
	// modelled (OPAQUE -> empty IL, matching the legacy lifter).
	{ .mask = 0xff000000, .match = 0xb6000000, .id = TMS320C55_INS_DELAY, .lop = C55_LOP_OPAQUE, .len = 2, .no_parallel = true, .ops = { { .lo = 0, .width = 8, .fn = c55x_x_smem } } },
	// psh Smem (opcode 0xb5) / pop Smem (opcode 0xbb) push / pop a single
	// data-memory word; psh dbl(Smem) (0xb7) / pop dbl(Smem) (0xb8) push / pop a
	// double word. Single Smem operand (the low byte is the addressing field,
	// not the parallel marker -> no_parallel); the analysis stack effect (one
	// word for the single forms, two for the dbl forms) comes from the PUSH /
	// POP op-type, and the IL is left to the legacy lifter (empty). Only the
	// duplicate 0xb9 pop dbl now stays on the legacy decoder.
	{ .mask = 0xff000000, .match = 0xb5000000, .id = TMS320C55_INS_PSH, .len = 2, .no_parallel = true, .ops = { { .lo = 0, .width = 8, .fn = c55x_x_smem } } },
	{ .mask = 0xff000000, .match = 0xbb000000, .id = TMS320C55_INS_POP, .len = 2, .no_parallel = true, .ops = { { .lo = 0, .width = 8, .fn = c55x_x_smem } } },
	{ .mask = 0xff000000, .match = 0xb7000000, .id = TMS320C55_INS_PSH, .len = 2, .no_parallel = true, .ops = { { .lo = 0, .width = 8, .fn = c55x_x_smem_dbl } } },
	{ .mask = 0xff000000, .match = 0xb8000000, .id = TMS320C55_INS_POP, .len = 2, .no_parallel = true, .ops = { { .lo = 0, .width = 8, .fn = c55x_x_smem_dbl } } },
	// psh / pop src1, src2 (opcode 0x38 psh, 0x3a pop; 0x39 / 0x3b parallel): push
	// or pop two independent gr4 registers in one instruction (byte1 bits 4-7 name
	// the first register, bits 0-3 the second). The two-register sequence is not
	// modelled by the shared push/pop lifter -- the .both flag leaves it to the
	// legacy lifter -- and the analysis records a fixed one-word SP delta (the
	// dual-form branch in c55_stack_words).
	{ .mask = 0xfe000000, .match = 0x38000000, .id = TMS320C55_INS_PSH, .len = 2, .both = true, .ops = { { .lo = 4, .fn = c55x_x_gr4 }, { .lo = 0, .fn = c55x_x_gr4 } } },
	{ .mask = 0xfe000000, .match = 0x3a000000, .id = TMS320C55_INS_POP, .len = 2, .both = true, .ops = { { .lo = 4, .fn = c55x_x_gr4 }, { .lo = 0, .fn = c55x_x_gr4 } } },
	// 0x54 register D-unit ALU family (two bytes): "<op>[r] [ACx,] ACy", a 3-bit
	// sub-opcode in the low byte's bits 1-3 selects the operation, bit 0 is the
	// rounding variant, ACx is bits 4-5 and the destination ACy is bits 6-7; the
	// leading byte's bit 0 is the parallel marker (left free). ACx collapses
	// against ACy when they are equal (the multiply/square then operates on ACy
	// alone -- handled in the MUL lift). The multiply / square members are lifted
	// here; addv (0), round (5) and sat (6) stay on the legacy decoder. The
	// accumulator multiplicand is the high word ACx(32-16) (see c55_mul_val).
	// mov hi(ACx), dst (opcode 0x44, high nibble 0-3): move an accumulator's
	// 16-bit high word, sign-extended, into the gr4 destination (low nibble).
	// 0x45 is the parallel form. The source ACx is byte1 bits 4-5.
	{ .mask = 0xfec00000, .match = 0x44000000, .id = TMS320C55_INS_MOV, .len = 2, .ops = { { .lo = 4, .fn = c55x_x_hi_ac }, { .lo = 0, .fn = c55x_x_gr4 } } },
	// sfts dst, #1 / #-1 (opcode 0x44, high nibble 4-7): a fixed +-1 arithmetic
	// shift of the gr4 register (low nibble). byte1 bit 4 selects the sign; the
	// shared SFTS handler lifts the accumulator forms (left for +1, arithmetic
	// right for -1).
	{ .mask = 0xfec00000, .match = 0x44400000, .id = TMS320C55_INS_SFTS, .lop = C55_LOP_SFTS, .len = 2, .ops = { { .lo = 0, .fn = c55x_x_gr4 }, { .fn = c55x_x_sfts_imm } } },
	// aadd #k8, sp (opcode 0x4e, 0x4f parallel): add a signed 8-bit constant to
	// the stack pointer (frame setup). k8 prints unsigned but is signed; the
	// shared ADD lifter adds it zero-extended (matching the legacy IL) and the
	// analysis records the signed SP delta.
	{ .mask = 0xfe000000, .match = 0x4e000000, .id = TMS320C55_INS_AADD, .len = 2, .ops = { { .lo = 0, .width = 8, .fn = c55_x_imm }, { .fn = c55x_x_sp } } },
	// mov gr4, hi(ACx) (opcode 0x52, 0x53 parallel): write a general register's
	// low 16 bits into an accumulator's high word. Byte-1 high nibble selects
	// the source (gr4: AC/T/AR), low nibble 0-3 selects the destination hi(AC0-3).
	// The special-register destinations (low nibble >= 8) are left to the legacy.
	{ .mask = 0xfe0c0000, .match = 0x52000000, .id = TMS320C55_INS_MOV, .len = 2, .ops = { { .lo = 4, .fn = c55x_x_gr4 }, { .lo = 0, .fn = c55x_x_hi_ac } } },
	// mov gr4, <special> (opcode 0x52, low nibble 8-15): write a general
	// register into sp/ssp/cdp/csr/brc1/brc0. An accumulator source is narrowed
	// to the 16-bit special register by the shared MOV lifter.
	{ .mask = 0xfe080000, .match = 0x52080000, .id = TMS320C55_INS_MOV, .len = 2, .ops = { { .lo = 4, .fn = c55x_x_gr4 }, { .lo = 0, .fn = c55x_x_52dst } } },
	// swap rX, rY (opcode 0x5e, 0x5f parallel): exchange a register pair via the
	// XOR-swap idiom. Byte-1 bit 4 = 0 single, = 1 swapp (also swaps the next
	// pair); bit 5 must be 0; bits 6-7 are ignored.
	{ .mask = 0xfe300000, .match = 0x5e000000, .id = TMS320C55_INS_SWAP, .len = 2, .ops = { { .lo = 0, .param = 0, .fn = c55x_x_swap }, { .lo = 0, .param = 1, .fn = c55x_x_swap } } },
	{ .mask = 0xfe300000, .match = 0x5e100000, .id = TMS320C55_INS_SWAPP, .both = true, .len = 2, .ops = { { .lo = 0, .param = 0, .fn = c55x_x_swap }, { .lo = 0, .param = 1, .fn = c55x_x_swap } } },
	{ .mask = 0xfe300000, .match = 0x5e200000, .id = TMS320C55_INS_SWAP4, .quad = true, .len = 2, .ops = { { .lo = 0, .param = 0, .fn = c55x_x_swap }, { .lo = 0, .param = 1, .fn = c55x_x_swap } } },
	// rpt / rptadd / rptsub (opcode 0x48, 0x49 parallel): single-instruction
	// repeat control over the CSR counter. byte1 low nibble selects the form
	// (0 rpt, 1 rptadd CSR/TAx, 2 rptadd CSR/k4, 3 rptsub CSR/k4); ret/reti at
	// nibble 4/5 are decoded elsewhere. The operand register/immediate is byte1
	// bits 4-7.
	{ .mask = 0xfe0f0000, .match = 0x48000000, .id = TMS320C55_INS_RPT, .lop = C55_LOP_NOP, .len = 2, .ops = { { .fn = c55x_x_csr } } },
	{ .mask = 0xfe0f0000, .match = 0x48010000, .id = TMS320C55_INS_RPTADD, .lop = C55_LOP_RPTADD, .len = 2, .ops = { { .fn = c55x_x_csr }, { .lo = 4, .fn = c55x_x_gr4 } } },
	{ .mask = 0xfe0f0000, .match = 0x48020000, .id = TMS320C55_INS_RPTADD, .lop = C55_LOP_RPTADD, .len = 2, .ops = { { .fn = c55x_x_csr }, { .lo = 4, .width = 4, .fn = c55_x_imm } } },
	{ .mask = 0xfe0f0000, .match = 0x48030000, .id = TMS320C55_INS_RPTSUB, .lop = C55_LOP_RPTSUB, .len = 2, .ops = { { .fn = c55x_x_csr }, { .lo = 4, .width = 4, .fn = c55_x_imm } } },
	// rpt #k16 (opcode 0x0c, 0x0d parallel): repeat the next instruction by a
	// 16-bit immediate count (byte1 the high byte, byte2 the low byte). It has no
	// data effect of its own, so it lifts to nop like the other repeat forms.
	{ .mask = 0xfe000000, .match = 0x0c000000, .id = TMS320C55_INS_RPT, .lop = C55_LOP_NOP, .len = 3, .ops = { { .lo = 0, .width = 16, .fn = c55_x_imm } } },
	// rptcc K8, cond (opcode 0x00, 0x01 parallel): conditionally repeat the next
	// instruction K8 times. byte2 is the count, byte1 the 7-bit condition.
	{ .mask = 0xfe000000, .match = 0x00000000, .id = TMS320C55_INS_RPTCC, .lop = C55_LOP_NOP, .len = 3, .ops = { { .lo = 0, .width = 8, .fn = c55_x_imm }, { .lo = 8, .width = 7, .fn = c55x_x_cond } } },
	// rptb L16 (opcode 0x0e, 0x0f parallel): block repeat to the 16-bit end
	// address held in byte1:byte2.
	{ .mask = 0xfe000000, .match = 0x0e000000, .id = TMS320C55_INS_RPTB, .lop = C55_LOP_NOP, .len = 3, .ops = { { .lo = 0, .width = 16, .fn = c55_x_imm } } },
	// amov / asub ACx, ACy register forms (opcode 0x14, 0x15 parallel): byte-2
	// bits 0-1 select the operation (00 aadd, 01 amov, 10 asub) and bit 2 picks
	// the register (0) vs immediate (1) source. Only the register amov/asub are
	// taken here (aadd's legacy LEA type would change under the shared AADD=ADD,
	// and the immediate forms stay on legacy). src is byte1 bits 4-7, dst byte2
	// bits 4-7. asub keeps the legacy null type; its IL comes from C55_LOP_AREG.
	{ .mask = 0xfe000700, .match = 0x14000100, .id = TMS320C55_INS_AMOV, .lop = C55_LOP_AREG_MOV, .len = 3, .ops = { { .lo = 12, .fn = c55x_x_gr4a }, { .lo = 4, .fn = c55x_x_gr4a } } },
	{ .mask = 0xfe000700, .match = 0x14000200, .id = TMS320C55_INS_ASUB, .lop = C55_LOP_AREG_SUB, .len = 3, .ops = { { .lo = 12, .fn = c55x_x_gr4a }, { .lo = 4, .fn = c55x_x_gr4a } } },
	// cmp / cmpand / cmpor SRC <relop> DST, [TCx,] TCz (opcode 0x12, 0x13
	// parallel): compare two gr4 registers and write a status TC bit. Byte-1
	// bits 0-1 select the variant (00 cmp, 01 cmpand, 10 cmpor; 11 is rol, left
	// to legacy); bits 2-3 the relop; bits 4-7 SRC. Byte-2 bit 0 = TCz, bit 1 =
	// TCx, bit 2 = unsigned (the 'u' variants), bits 4-7 = DST.
	{ .mask = 0xfe030400, .match = 0x12000000, .id = TMS320C55_INS_CMP, .lop = C55_LOP_CMP, .len = 3, .ops = { { .fn = c55x_x_cmpcond }, { .lo = 0, .fn = c55x_x_tcflag } } },
	{ .mask = 0xfe030400, .match = 0x12000400, .id = TMS320C55_INS_CMP, .lop = C55_LOP_CMP, .len = 3, .uns_all = true, .ops = { { .fn = c55x_x_cmpcond }, { .lo = 0, .fn = c55x_x_tcflag } } },
	{ .mask = 0xfe030400, .match = 0x12010000, .id = TMS320C55_INS_CMPAND, .lop = C55_LOP_CMPAND, .len = 3, .ops = { { .fn = c55x_x_cmpcond }, { .lo = 1, .param = 3, .fn = c55x_x_tcflag }, { .lo = 0, .fn = c55x_x_tcflag } } },
	{ .mask = 0xfe030400, .match = 0x12010400, .id = TMS320C55_INS_CMPAND, .lop = C55_LOP_CMPAND, .len = 3, .uns_all = true, .ops = { { .fn = c55x_x_cmpcond }, { .lo = 1, .param = 3, .fn = c55x_x_tcflag }, { .lo = 0, .fn = c55x_x_tcflag } } },
	{ .mask = 0xfe030400, .match = 0x12020000, .id = TMS320C55_INS_CMPOR, .lop = C55_LOP_CMPOR, .len = 3, .ops = { { .fn = c55x_x_cmpcond }, { .lo = 1, .param = 3, .fn = c55x_x_tcflag }, { .lo = 0, .fn = c55x_x_tcflag } } },
	{ .mask = 0xfe030400, .match = 0x12020400, .id = TMS320C55_INS_CMPOR, .lop = C55_LOP_CMPOR, .len = 3, .uns_all = true, .ops = { { .fn = c55x_x_cmpcond }, { .lo = 1, .param = 3, .fn = c55x_x_tcflag }, { .lo = 0, .fn = c55x_x_tcflag } } },
	// rol / ror BitIn, ACx, BitOut, ACy (opcode 0x12, byte1 bits 0-1 = 11):
	// rotate the accumulator left/right by one through a status bit. byte2 bit 3
	// selects ror (1) over rol (0); SRC is byte1 bits 4-7, DST byte2 bits 4-7.
	// For rol, byte2 bit 0 is the rotate-in bit and bit 1 the rotate-out bit;
	// the legacy swaps these for ror (bit 1 = rotate-in, bit 0 = rotate-out).
	// Each bit picks carry (0) or tc2 (1). The legacy models the data effect only
	// for accumulator src+dst, so T/AR forms fall through to a null lift.
	{ .mask = 0xfe030800, .match = 0x12030000, .id = TMS320C55_INS_ROL, .lop = C55_LOP_ROL, .len = 3, .ops = { { .lo = 0, .fn = c55x_x_rolflag }, { .lo = 12, .width = 4, .fn = c55x_x_gr4 }, { .lo = 1, .fn = c55x_x_rolflag }, { .lo = 4, .width = 4, .fn = c55x_x_gr4 } } },
	{ .mask = 0xfe030800, .match = 0x12030800, .id = TMS320C55_INS_ROR, .lop = C55_LOP_ROR, .len = 3, .ops = { { .lo = 1, .fn = c55x_x_rolflag }, { .lo = 12, .width = 4, .fn = c55x_x_gr4 }, { .lo = 0, .fn = c55x_x_rolflag }, { .lo = 4, .width = 4, .fn = c55x_x_gr4 } } },
	// bfxtr / bfxpa K16, ACx, ACy (opcode 0x76): bit-field extract / expand.
	// bfxtr extracts the bits of ACx selected by the K16 mask and right-packs
	// them into ACy; bfxpa is the inverse (expand-and-pack). 0x76 is multi-form
	// -- byte3 bits 2-3 select the operation (0 bfxtr, 1 bfxpa; 2 is a mov form
	// and 3 is invalid, both left to the legacy decoder). K16 is bytes 1-2, the
	// AC source is byte3 bits 0-1, and the gr4 destination is byte3 bits 4-7.
	// bfxtr is typed MOV (a field move); bfxpa is untyped, as the legacy did.
	// Neither is lifted (OPAQUE).
	{ .mask = 0xff00000c, .match = 0x76000000, .id = TMS320C55_INS_BFXTR, .lop = C55_LOP_OPAQUE, .len = 4, .no_parallel = true, .ops = { { .lo = 8, .width = 16, .fn = c55_x_imm }, { .lo = 0, .width = 2, .param = C55_RC_AC, .fn = c55_x_reg }, { .lo = 4, .fn = c55x_x_gr4 } } },
	{ .mask = 0xff00000c, .match = 0x76000004, .id = TMS320C55_INS_BFXPA, .lop = C55_LOP_OPAQUE, .len = 4, .no_parallel = true, .ops = { { .lo = 8, .width = 16, .fn = c55_x_imm }, { .lo = 0, .width = 2, .param = C55_RC_AC, .fn = c55_x_reg }, { .lo = 4, .fn = c55x_x_gr4 } } },
	// amov #k16, dst (opcode 0x77, no parallel): load a zero-extended 16-bit
	// constant/address. k16 is bits 8-23 (byte1:byte2); the gr4 destination
	// selector is byte3 bits 4-7 (0-3 AC, 4-7 T, 8-15 AR).
	{ .mask = 0xff000000, .match = 0x77000000, .id = TMS320C55_INS_AMOV, .lop = C55_LOP_AMOV, .len = 4, .no_parallel = true, .ops = { { .lo = 8, .width = 16, .fn = c55_x_imm }, { .lo = 4, .fn = c55x_x_gr4 } } },
	// mpyk / mpykr #k, ACx[, ACy] (opcodes 0x1e 3-byte K8, 0x79 4-byte K16;
	// 0x1e has a parallel companion 0x1f rendered "|| mpyk"): ACy = #k * ACx,
	// the signed constant multiplying the low 16 bits of ACx. The variant byte
	// (byte2 for 0x1e, byte3 for 0x79) holds ACx in bits 6-7, ACy in bits 4-5,
	// bit 1 = 0 selecting the mpyk family (1 = mack), and bit 0 = the round (r)
	// suffix (read via mods). ACy defaults to ACx (elided when equal). The
	// 4-byte 0x79 has no parallel form (0x78 is a different opcode), so it pins
	// byte0 fully.
	{ .mask = 0xfe000200, .match = 0x1e000000, .id = TMS320C55_INS_MPYK, .lop = C55_LOP_MPYK, .len = 3, .mods = 1, .ops = { { .lo = 8, .width = 8, .fn = c55_x_imm }, { .lo = 6, .fn = c55x_x_ac2 }, { .lo = 4, .fn = c55x_x_ac2_elide } } },
	{ .mask = 0xff000002, .match = 0x79000000, .id = TMS320C55_INS_MPYK, .lop = C55_LOP_MPYK, .len = 4, .no_parallel = true, .mods = 1, .ops = { { .lo = 8, .width = 16, .fn = c55_x_imm }, { .lo = 6, .fn = c55x_x_ac2 }, { .lo = 4, .fn = c55x_x_ac2_elide } } },
	// mack / mackr Tx, #k, ACx[, ACy] (variant byte bit 1 = 1): ACy = ACx + #k *
	// Tx. Same encoding as mpyk but the Tx coefficient (variant-byte bits 2-3)
	// replaces the accumulator low word as the multiplicand; bit 0 = the round
	// (r) suffix. ACy defaults to ACx (elided when equal).
	{ .mask = 0xfe000200, .match = 0x1e000200, .id = TMS320C55_INS_MACK, .lop = C55_LOP_MACK, .len = 3, .mods = 1, .ops = { { .lo = 2, .fn = c55x_x_t2 }, { .lo = 8, .width = 8, .fn = c55_x_imm }, { .lo = 6, .fn = c55x_x_ac2 }, { .lo = 4, .fn = c55x_x_ac2_elide } } },
	{ .mask = 0xff000002, .match = 0x79000002, .id = TMS320C55_INS_MACK, .lop = C55_LOP_MACK, .len = 4, .no_parallel = true, .mods = 1, .ops = { { .lo = 2, .fn = c55x_x_t2 }, { .lo = 8, .width = 16, .fn = c55_x_imm }, { .lo = 6, .fn = c55x_x_ac2 }, { .lo = 4, .fn = c55x_x_ac2_elide } } },
	// bclr / bset #k4, STx (opcode 0x46, 0x47 parallel): clear or set bit #k4 of
	// a status register. Byte-1 bit 0 = 0 bclr, = 1 bset; bits 1-3 = STn_55;
	// bits 4-7 = the 4-bit bit index #k4.
	{ .mask = 0xfe010000, .match = 0x46000000, .id = TMS320C55_INS_BCLR, .lop = C55_LOP_BITCLR, .len = 2, .ops = { { .lo = 4, .width = 4, .fn = c55_x_imm }, { .lo = 1, .fn = c55x_x_st } } },
	{ .mask = 0xfe010000, .match = 0x46010000, .id = TMS320C55_INS_BSET, .lop = C55_LOP_BITSET, .len = 2, .ops = { { .lo = 4, .width = 4, .fn = c55_x_imm }, { .lo = 1, .fn = c55x_x_st } } },
	// add/sub ACx << Tx, ACy (opcode 0x5a, 0x5b parallel): ACy +/- (ACx shifted
	// left by the Tx register). Byte-1 bit 0 selects add (0) vs sub (1); bit 1
	// must be 0 (bit 1 = 1 selects the sftcc forms, left to the legacy decoder).
	// bits 2-3 = Tx, 4-5 = source ACx, 6-7 = destination ACy.
	{ .mask = 0xfe030000, .match = 0x5a000000, .id = TMS320C55_INS_ADD, .lop = C55_LOP_ADDSHL, .len = 2, .ops = { { .lo = 4, .width = 2, .param = C55_RC_AC, .fn = c55_x_reg }, { .lo = 2, .fn = c55x_x_tx_shl }, { .lo = 6, .width = 2, .param = C55_RC_AC, .fn = c55_x_reg } } },
	{ .mask = 0xfe030000, .match = 0x5a010000, .id = TMS320C55_INS_SUB, .lop = C55_LOP_SUBSHL, .len = 2, .ops = { { .lo = 4, .width = 2, .param = C55_RC_AC, .fn = c55_x_reg }, { .lo = 2, .fn = c55x_x_tx_shl }, { .lo = 6, .width = 2, .param = C55_RC_AC, .fn = c55_x_reg } } },
	// sftcc ACx, TCx (opcode 0x5a, byte1 bit1 = 1): test the sign of an
	// accumulator and copy it into a test-control bit (TI shift-conditional
	// support op). ACx is byte1 bits 6-7; TCx is byte1 bit 0 (0 -> tc1, 1 ->
	// tc2), rendered lowercase via the condition-flag path. The add / sub-shift
	// forms occupy byte1 bit1 = 0, so this match is disjoint from them. Null
	// type and IL (OPAQUE), matching the legacy decoder.
	{ .mask = 0xfe020000, .match = 0x5a020000, .id = TMS320C55_INS_SFTCC, .lop = C55_LOP_OPAQUE, .len = 2, .ops = { { .lo = 6, .width = 2, .param = C55_RC_AC, .fn = c55_x_reg }, { .lo = 0, .fn = c55x_x_tcflag } } },
	// mov <special>, dst (opcode 0x44, high nibble 8-15): move a 16-bit special
	// register (sp / ssp / cdp / brc0 / brc1 / rptc), sign-extended, into the
	// gr4 destination. The source extractor leaves the unassigned high nibbles
	// (11, 15) to the legacy decoder.
	{ .mask = 0xfe800000, .match = 0x44800000, .id = TMS320C55_INS_MOV, .len = 2, .ops = { { .lo = 4, .fn = c55x_x_44src }, { .lo = 0, .fn = c55x_x_gr4 } } },
	// sftl dst, #1 / #-1 (opcode 0x50, sub-opcodes 0/1): a fixed +-1 logical
	// shift of the gr4 register. Sub-opcode bit 0 (matched loosely below)
	// selects the count's sign; the shared SFTL handler lifts the accumulator
	// forms (a positive count shifts left, a negative one right).
	{ .mask = 0xfe060000, .match = 0x50000000, .id = TMS320C55_INS_SFTL, .lop = C55_LOP_SFTL, .len = 2, .ops = { { .lo = 4, .fn = c55x_x_gr4 }, { .fn = c55x_x_sftl_imm } } },
	// pop / psh ACx (opcode 0x50, sub-opcodes 2/3/6/7): single-accumulator
	// stack ops. The plain forms (2/6) take the gr4 register field (byte1
	// bits 4-7); the dbl(ACx) forms (3/7) are accumulator-only. The operand
	// moves as 32 bits / two stack words for an accumulator and one word for a
	// 16-bit register through the shared push/pop lifter. 0x51 is the parallel
	// form, so the opcode byte is matched modulo its low (parallel) bit.
	{ .mask = 0xfe070000, .match = 0x50020000, .id = TMS320C55_INS_POP, .len = 2, .ops = { { .lo = 4, .fn = c55x_x_gr4 } } },
	{ .mask = 0xfe070000, .match = 0x50030000, .id = TMS320C55_INS_POP, .len = 2, .ops = { { .lo = 4, .fn = c55x_x_ac_dbl } } },
	{ .mask = 0xfe070000, .match = 0x50060000, .id = TMS320C55_INS_PSH, .len = 2, .ops = { { .lo = 4, .fn = c55x_x_gr4 } } },
	{ .mask = 0xfe070000, .match = 0x50070000, .id = TMS320C55_INS_PSH, .len = 2, .ops = { { .lo = 4, .fn = c55x_x_ac_dbl } } },
	// popboth / pshboth xdst (opcode 0x50, sub-opcodes 4/5): pop / push a
	// register *pair* named by the extended-register field (byte1 bits 4-7).
	// The pair semantics are not lifted (the .both flag), but the stack effect
	// (two words) and pop/push type are modelled.
	{ .mask = 0xfe0f0000, .match = 0x50040000, .id = TMS320C55_INS_POPBOTH, .len = 2, .both = true, .ops = { { .lo = 4, .fn = c55x_x_xgr4 } } },
	{ .mask = 0xfe0f0000, .match = 0x50050000, .id = TMS320C55_INS_PSHBOTH, .len = 2, .both = true, .ops = { { .lo = 4, .fn = c55x_x_xgr4 } } },
	// sub-opcode 3: mpy[r] -- ACy = ACx * ACy.
	{ .mask = 0xfe0e0000, .match = 0x54060000, .id = TMS320C55_INS_MPY, .len = 2, .mods = 1, .ops = { { .fn = c55x_x_mpy_acsrc }, { .fn = c55x_x_mpy_acdst } } },
	// sub-opcode 4: sqr[r] -- ACy = ACx * ACx.
	{ .mask = 0xfe0e0000, .match = 0x54080000, .id = TMS320C55_INS_SQR, .len = 2, .mods = 1, .square = true, .ops = { { .fn = c55x_x_mpy_acsrc }, { .fn = c55x_x_mpy_acdst } } },
	// sub-opcode 1: sqa[r] -- ACy = ACy + ACx * ACx.
	{ .mask = 0xfe0e0000, .match = 0x54020000, .id = TMS320C55_INS_SQA, .lop = C55_LOP_MAC, .len = 2, .mods = 1, .square = true, .ops = { { .fn = c55x_x_mpy_acsrc }, { .fn = c55x_x_mpy_acdst } } },
	// sub-opcode 2: sqs[r] -- ACy = ACy - ACx * ACx.
	{ .mask = 0xfe0e0000, .match = 0x54040000, .id = TMS320C55_INS_SQS, .lop = C55_LOP_MAS, .len = 2, .mods = 1, .square = true, .ops = { { .fn = c55x_x_mpy_acsrc }, { .fn = c55x_x_mpy_acdst } } },
	// sub-opcode 0: addv / addrv ACx, ACy -- ACy = ACy + |ACx(32-16)| (addition
	// with absolute value). The rounded variant is spelled "addrv" (the r is
	// infixed, not a trailing suffix), so it is a distinct id selected by bit 0
	// rather than the generic rounding-suffix mods; bit 0 is therefore pinned.
	{ .mask = 0xfe0f0000, .match = 0x54000000, .id = TMS320C55_INS_ADDV, .lop = C55_LOP_ADDV, .len = 2, .ops = { { .fn = c55x_x_mpy_acsrc }, { .fn = c55x_x_mpy_acdst } } },
	{ .mask = 0xfe0f0000, .match = 0x54010000, .id = TMS320C55_INS_ADDRV, .lop = C55_LOP_ADDV, .len = 2, .ops = { { .fn = c55x_x_mpy_acsrc }, { .fn = c55x_x_mpy_acdst } } },
	// sub-opcode 5: round ACx, ACy -- ACy = round(ACx). The low byte's bit 0 is
	// don't-care here (the operation always rounds, so both encodings print
	// "round"); no rounding-suffix mods, leaving the type untyped as the legacy.
	{ .mask = 0xfe0e0000, .match = 0x540a0000, .id = TMS320C55_INS_ROUND, .lop = C55_LOP_ROUND, .len = 2, .ops = { { .fn = c55x_x_mpy_acsrc }, { .fn = c55x_x_mpy_acdst } } },
	// sub-opcode 6: sat[r] ACx, ACy -- ACy = saturate(ACx). bit 0 selects the
	// rounding variant (satr) via mods.
	{ .mask = 0xfe0e0000, .match = 0x540c0000, .id = TMS320C55_INS_SAT, .lop = C55_LOP_SAT, .len = 2, .mods = 1, .ops = { { .fn = c55x_x_mpy_acsrc }, { .fn = c55x_x_mpy_acdst } } },
	// mac[r] ACx, Tx, ACy[, ACy] and mas[r] Tx, [ACx,] ACy (opcode 0x56, two
	// bytes): the register MAC / MAS that take a T-register coefficient, ACy =
	// ACy [+/-] sx17(ACx(32-16)) * sx40(Tx). The low byte holds round (bit 0),
	// the mac/mas selector (bit 1: 0 mac, 1 mas), Tx (bits 2-3), the ACx
	// multiplicand (bits 4-5) and the destination ACy (bits 6-7); the leading
	// byte's bit 0 is the parallel marker (left free). mac prints the trailing
	// ACy only when ACx differs from it; mas drops the ACx when it equals ACy.
	{ .mask = 0xfe020000, .match = 0x56000000, .id = TMS320C55_INS_MAC, .lop = C55_LOP_MAC, .len = 2, .mods = 1, .ops = { { .lo = 4, .width = 2, .param = C55_RC_AC, .fn = c55_x_reg }, { .lo = 2, .width = 2, .param = C55_RC_T, .fn = c55_x_reg }, { .lo = 6, .width = 2, .param = C55_RC_AC, .fn = c55_x_reg }, { .fn = c55x_x_macreg_acy_dst } } },
	{ .mask = 0xfe020000, .match = 0x56020000, .id = TMS320C55_INS_MAS, .lop = C55_LOP_MAS, .len = 2, .mods = 1, .ops = { { .lo = 2, .width = 2, .param = C55_RC_T, .fn = c55_x_reg }, { .fn = c55x_x_mpy_acsrc }, { .lo = 6, .width = 2, .param = C55_RC_AC, .fn = c55_x_reg } } },
	// mpy[r] Tx, [ACy,] ACx (opcode 0x58, two bytes): a register multiply,
	// ACx = sx40(Tx) * sx17(ACy(32-16)) -- the accumulator multiplicand is its
	// high word, not its low half (see c55_mul_val). An optional rounding variant.
	// The leading byte's bit 0 is the parallel marker (so 0x58 / 0x59 both match,
	// with that bit left free); the low byte's bit 0 is the round flag (folded in
	// via mods so 0x58.. and its mpyr sibling share this row) and bit 1 selects the
	// accumulate form mac[r] (the next row).
	{ .mask = 0xfe020000, .match = 0x58000000, .id = TMS320C55_INS_MPY, .len = 2, .mods = 1, .ops = { { .fn = c55x_x_mpy_t }, { .fn = c55x_x_mpy_acsrc }, { .fn = c55x_x_mpy_acdst } } },
	// mac[r] ACy, Tx, ACx, ACy (opcode 0x58, low byte's bit 1 = 1): the register
	// MAC, ACy = ACx + sx17(ACy(32-16)) * sx40(Tx), rounded for macr. ACy (bits 6-7)
	// is both the multiplicand high word and the destination; ACx (bits 4-5) is the
	// accumulator addend; Tx is bits 2-3. Unlike the multiply this form always
	// prints all four operands. The legacy lifted the multiplicand as ACy.l; this
	// matches the multiply in using the high word.
	{ .mask = 0xfe020000, .match = 0x58020000, .id = TMS320C55_INS_MAC, .lop = C55_LOP_MAC, .len = 2, .mods = 1, .ops = { { .lo = 6, .width = 2, .param = C55_RC_AC, .fn = c55_x_reg }, { .lo = 2, .width = 2, .param = C55_RC_T, .fn = c55_x_reg }, { .lo = 4, .width = 2, .param = C55_RC_AC, .fn = c55_x_reg }, { .lo = 6, .width = 2, .param = C55_RC_AC, .fn = c55_x_reg } } },
	// sftl / sfts / sftsc ACx, Tx[, ACy] (opcode 0x5c, two bytes): a register
	// shift of ACx by the signed count in Tx, written to ACy. The low byte holds
	// the sub-opcode (bits 0-1: 0 sftl, 1 sfts, 2 sftsc; 3 is invalid), Tx (bits
	// 2-3), the source ACx (bits 4-5) and the destination ACy (bits 6-7); the
	// leading byte's bit 0 is the parallel marker (left free). ACy collapses
	// against ACx when they are equal. sftl shifts logically and sfts shifts
	// arithmetically (lifted here); sftsc additionally affects the carry and is
	// left unlifted, matching the legacy.
	{ .mask = 0xfe030000, .match = 0x5c000000, .id = TMS320C55_INS_SFTL, .lop = C55_LOP_SFTL, .len = 2, .ops = { { .lo = 4, .width = 2, .param = C55_RC_AC, .fn = c55_x_reg }, { .lo = 2, .width = 2, .param = C55_RC_T, .fn = c55_x_reg }, { .fn = c55x_x_macreg_acy_dst } } },
	{ .mask = 0xfe030000, .match = 0x5c010000, .id = TMS320C55_INS_SFTS, .lop = C55_LOP_SFTS, .len = 2, .ops = { { .lo = 4, .width = 2, .param = C55_RC_AC, .fn = c55_x_reg }, { .lo = 2, .width = 2, .param = C55_RC_T, .fn = c55_x_reg }, { .fn = c55x_x_macreg_acy_dst } } },
	{ .mask = 0xfe030000, .match = 0x5c020000, .id = TMS320C55_INS_SFTSC, .len = 2, .ops = { { .lo = 4, .width = 2, .param = C55_RC_AC, .fn = c55_x_reg }, { .lo = 2, .width = 2, .param = C55_RC_T, .fn = c55_x_reg }, { .fn = c55x_x_macreg_acy_dst } } },
	// sfts / sftsc / sftl ACx, #SHIFTW[, ACy] (opcode 0x10, three bytes): the
	// immediate-count companions of the 0x5c register shifts. The leading byte
	// is the opcode (bit 0 the parallel marker); the middle byte holds the
	// sub-opcode (bits 0-3: 5 sfts, 6 sftsc, 7 sftl -- the lower sub-opcodes are
	// the and/or/xor/add/sub shift-and-combine forms, still on the legacy
	// decoder), the source ACx (bits 4-5) and the destination ACy (bits 6-7);
	// the last byte's bits 0-5 are the signed shift count SHIFTW. ACy collapses
	// against ACx when equal. sftl/sfts are lifted (sign-extending SHIFTW to pick
	// the direction); sftsc is left unlifted like the legacy.
	// and / or / xor / add / sub ACx << #SHIFTW[, ACy] (opcode 0x10, sub-opcodes
	// 0-4): shift-and-combine forms sharing the 0x10 encoding with the shifts
	// below. ACx is byte1 bits 4-5, the destination ACy bits 6-7, SHIFTW the last
	// byte's bits 0-5. The bitwise forms (and/or/xor) print the trailing ACy only
	// when it differs from ACx and collapse otherwise; add/sub always print it.
	// exp ACx, Tx (opcode 0x10, byte1 nibble 0x8): compute the exponent (the
	// leading-sign-bit count) of ACx into Tx. ACx is byte1 bits 4-5, Tx byte2
	// bits 4-5. The shift computation is not modelled, so it decodes and analyses
	// on the shared path (REP-free, default null type) but carries no IL.
	{ .mask = 0xfe0f0000, .match = 0x10080000, .id = TMS320C55_INS_EXP, .lop = C55_LOP_OPAQUE, .len = 3, .ops = { { .lo = 12, .width = 2, .param = C55_RC_AC, .fn = c55_x_reg }, { .lo = 4, .width = 2, .param = C55_RC_T, .fn = c55_x_reg } } },
	// bcnt ACx, ACy, TCx, Tx (opcode 0x10, byte1 nibble 0xa): count bits; null
	// analysis type and no modelled effect, so OPAQUE. ACx byte1 bits4-5, ACy
	// byte2 bits6-7, TCx byte2 bit0, Tx byte2 bits4-5.
	{ .mask = 0xfe0f0000, .match = 0x100a0000, .id = TMS320C55_INS_BCNT, .lop = C55_LOP_OPAQUE, .len = 3, .ops = { { .lo = 12, .width = 2, .param = C55_RC_AC, .fn = c55_x_reg }, { .lo = 6, .width = 2, .param = C55_RC_AC, .fn = c55_x_reg }, { .lo = 0, .fn = c55x_x_tcflag }, { .lo = 4, .width = 2, .param = C55_RC_T, .fn = c55_x_reg } } },
	// maxdiff / mindiff ACx, ACy, ACz, ACw (opcode 0x10, nibbles 0xc / 0xe): the
	// four accumulators are interleaved across byte1/byte2 (ACx byte1 bits4-5,
	// ACy byte2 bits6-7, ACz byte1 bits6-7, ACw byte2 bits4-5). OPAQUE.
	{ .mask = 0xfe0f0000, .match = 0x100c0000, .id = TMS320C55_INS_MAXDIFF, .lop = C55_LOP_OPAQUE, .len = 3, .ops = { { .lo = 12, .width = 2, .param = C55_RC_AC, .fn = c55_x_reg }, { .lo = 6, .width = 2, .param = C55_RC_AC, .fn = c55_x_reg }, { .lo = 14, .width = 2, .param = C55_RC_AC, .fn = c55_x_reg }, { .lo = 4, .width = 2, .param = C55_RC_AC, .fn = c55_x_reg } } },
	{ .mask = 0xfe0f0000, .match = 0x100e0000, .id = TMS320C55_INS_MINDIFF, .lop = C55_LOP_OPAQUE, .len = 3, .ops = { { .lo = 12, .width = 2, .param = C55_RC_AC, .fn = c55_x_reg }, { .lo = 6, .width = 2, .param = C55_RC_AC, .fn = c55_x_reg }, { .lo = 14, .width = 2, .param = C55_RC_AC, .fn = c55_x_reg }, { .lo = 4, .width = 2, .param = C55_RC_AC, .fn = c55_x_reg } } },
	// dmaxdiff / dmindiff ACx, ACy, ACz, ACw, TRNx (opcode 0x10, nibbles 0xd /
	// 0xf): as max/mindiff plus the trn0/trn1 transition register at byte2 bit0.
	{ .mask = 0xfe0f0000, .match = 0x100d0000, .id = TMS320C55_INS_DMAXDIFF, .lop = C55_LOP_OPAQUE, .len = 3, .ops = { { .lo = 12, .width = 2, .param = C55_RC_AC, .fn = c55_x_reg }, { .lo = 6, .width = 2, .param = C55_RC_AC, .fn = c55_x_reg }, { .lo = 14, .width = 2, .param = C55_RC_AC, .fn = c55_x_reg }, { .lo = 4, .width = 2, .param = C55_RC_AC, .fn = c55_x_reg }, { .lo = 0, .fn = c55x_x_trn } } },
	{ .mask = 0xfe0f0000, .match = 0x100f0000, .id = TMS320C55_INS_DMINDIFF, .lop = C55_LOP_OPAQUE, .len = 3, .ops = { { .lo = 12, .width = 2, .param = C55_RC_AC, .fn = c55_x_reg }, { .lo = 6, .width = 2, .param = C55_RC_AC, .fn = c55_x_reg }, { .lo = 14, .width = 2, .param = C55_RC_AC, .fn = c55_x_reg }, { .lo = 4, .width = 2, .param = C55_RC_AC, .fn = c55_x_reg }, { .lo = 0, .fn = c55x_x_trn } } },
	{ .mask = 0xfe0f0000, .match = 0x10000000, .id = TMS320C55_INS_AND, .lop = C55_LOP_ANDSHL, .len = 3, .ops = { { .lo = 12, .width = 2, .param = C55_RC_AC, .fn = c55_x_reg }, { .fn = c55x_x_shiftw_shl }, { .fn = c55x_x_shiftk_acy } } },
	{ .mask = 0xfe0f0000, .match = 0x10010000, .id = TMS320C55_INS_OR, .lop = C55_LOP_ORSHL, .len = 3, .ops = { { .lo = 12, .width = 2, .param = C55_RC_AC, .fn = c55_x_reg }, { .fn = c55x_x_shiftw_shl }, { .fn = c55x_x_shiftk_acy } } },
	{ .mask = 0xfe0f0000, .match = 0x10020000, .id = TMS320C55_INS_XOR, .lop = C55_LOP_XORSHL, .len = 3, .ops = { { .lo = 12, .width = 2, .param = C55_RC_AC, .fn = c55_x_reg }, { .fn = c55x_x_shiftw_shl }, { .fn = c55x_x_shiftk_acy } } },
	{ .mask = 0xfe0f0000, .match = 0x10030000, .id = TMS320C55_INS_ADD, .lop = C55_LOP_ADDSHL, .len = 3, .ops = { { .lo = 12, .width = 2, .param = C55_RC_AC, .fn = c55_x_reg }, { .fn = c55x_x_shiftw_shl }, { .lo = 14, .width = 2, .param = C55_RC_AC, .fn = c55_x_reg } } },
	{ .mask = 0xfe0f0000, .match = 0x10040000, .id = TMS320C55_INS_SUB, .lop = C55_LOP_SUBSHL, .len = 3, .ops = { { .lo = 12, .width = 2, .param = C55_RC_AC, .fn = c55_x_reg }, { .fn = c55x_x_shiftw_shl }, { .lo = 14, .width = 2, .param = C55_RC_AC, .fn = c55_x_reg } } },
	{ .mask = 0xfe0f0000, .match = 0x10050000, .id = TMS320C55_INS_SFTS, .lop = C55_LOP_SFTS, .len = 3, .ops = { { .lo = 12, .width = 2, .param = C55_RC_AC, .fn = c55_x_reg }, { .fn = c55x_x_shiftw }, { .fn = c55x_x_shiftk_acy } } },
	{ .mask = 0xfe0f0000, .match = 0x10060000, .id = TMS320C55_INS_SFTSC, .len = 3, .ops = { { .lo = 12, .width = 2, .param = C55_RC_AC, .fn = c55_x_reg }, { .fn = c55x_x_shiftw }, { .fn = c55x_x_shiftk_acy } } },
	{ .mask = 0xfe0f0000, .match = 0x10070000, .id = TMS320C55_INS_SFTL, .lop = C55_LOP_SFTL, .len = 3, .ops = { { .lo = 12, .width = 2, .param = C55_RC_AC, .fn = c55_x_reg }, { .fn = c55x_x_shiftw }, { .fn = c55x_x_shiftk_acy } } },
	// mpy[r] / mac[r] / mas[r] Smem, uns(Cmem), ACx (opcode 0xd0, three bytes):
	// the unsigned-coefficient memory multiplies, ACx [+/-]= sx40(Smem) *
	// zx40(Cmem). The last byte's bits 2-3 select the operation (01 mpy, 10 mac,
	// 11 mas); the coefficient is the (zero-extended) Cmem (bits 0-1 the CDP
	// post-modify), the destination ACx is bits 4-5, round is bit 6. The signed
	// macmz form (operation 00) has no dedicated id and stays on the legacy
	// decoder, as do the *(cdp+t0) coefficient mode and the side-load (bit 15)
	// forms.
	{ .mask = 0xff008c00, .match = 0xd0000400, .id = TMS320C55_INS_MPY, .len = 3, .no_parallel = true, .mods = 7, .ops = { { .lo = 8, .width = 8, .fn = c55x_x_smem }, { .fn = c55x_x_cmem, .param = 1 }, { .fn = c55x_x_mac_acdst } } },
	{ .mask = 0xff008c00, .match = 0xd0000800, .id = TMS320C55_INS_MAC, .lop = C55_LOP_MAC, .len = 3, .no_parallel = true, .mods = 7, .ops = { { .lo = 8, .width = 8, .fn = c55x_x_smem }, { .fn = c55x_x_cmem, .param = 1 }, { .fn = c55x_x_mac_acdst } } },
	{ .mask = 0xff008c00, .match = 0xd0000c00, .id = TMS320C55_INS_MAS, .lop = C55_LOP_MAS, .len = 3, .no_parallel = true, .mods = 7, .ops = { { .lo = 8, .width = 8, .fn = c55x_x_smem }, { .fn = c55x_x_cmem, .param = 1 }, { .fn = c55x_x_mac_acdst } } },
	// mpym[r] Smem, Cmem, ACx (opcode 0xd1, three bytes): a memory multiply,
	// ACx = sx40(Smem) * sx40(Cmem). Byte1 is the Smem field; the last byte
	// holds the Cmem coefficient mode (bits 0-1), the operation (bits 2-3:
	// 00 mpym), the destination ACx (bits 4-5), round (bit 6, folded in via
	// mods) and uns (bit 7). In the top-aligned match word the last byte sits
	// at bits 8-15, so op is pinned at bits 10-11 and uns at bit 15: this row
	// matches op = 00 (mpym, no accumulate) with uns = 0, leaving macm / masm
	// (op 01 / 10), the uns forms and the *(cdp+t0) coefficient mode (abandoned
	// by the Cmem extractor) to the legacy decoder.
	{ .mask = 0xff000c00, .match = 0xd1000000, .id = TMS320C55_INS_MPYM, .len = 3, .no_parallel = true, .mods = 0x207, .ops = { { .lo = 8, .width = 8, .fn = c55x_x_smem }, { .fn = c55x_x_cmem }, { .fn = c55x_x_mac_acdst } } },
	// macm[r] Smem, Cmem, ACx (op = 01, match bits 10-11 = 01): ACx += Smem*Cmem,
	// and masm[r] (op = 10): ACx -= Smem*Cmem. Same operands and uns / coefficient
	// constraints as mpym; the accumulate is carried via the MAC / MAS lift ops.
	{ .mask = 0xff000c00, .match = 0xd1000400, .id = TMS320C55_INS_MACM, .lop = C55_LOP_MAC, .len = 3, .no_parallel = true, .mods = 0x207, .ops = { { .lo = 8, .width = 8, .fn = c55x_x_smem }, { .fn = c55x_x_cmem }, { .fn = c55x_x_mac_acdst } } },
	{ .mask = 0xff000c00, .match = 0xd1000800, .id = TMS320C55_INS_MASM, .lop = C55_LOP_MAS, .len = 3, .no_parallel = true, .mods = 0x207, .ops = { { .lo = 8, .width = 8, .fn = c55x_x_smem }, { .fn = c55x_x_cmem }, { .fn = c55x_x_mac_acdst } } },
	// macm[r] / masm[r] Smem, [ACx,] ACy (opcode 0xd2, three bytes): a memory
	// MAC whose coefficient is the source accumulator high word,
	// ACy += sx40(Smem) * sx17(ACx(32-16)) (op = 00) or ACy -= ... (op = 01); the
	// squaring sqam[r] / sqsm[r] forms (op = 10 / 11, below) instead multiply
	// Smem by itself, ACy = ACx +/- sx40(Smem) * sx40(Smem). The last byte holds
	// the source ACx (bits 0-1), the operation (bits 2-3), the destination ACy
	// (bits 4-5), round (bit 6) and the side-load flag (bit 7). For the MACs only
	// the distinct-register three-operand form is lifted: the two-operand
	// (ACx == ACy) form has no legacy IL and stays on the legacy decoder.
	{ .mask = 0xff000c00, .match = 0xd2000000, .id = TMS320C55_INS_MACM, .lop = C55_LOP_MAC, .len = 3, .no_parallel = true, .mods = 0x207, .ops = { { .lo = 8, .width = 8, .fn = c55x_x_smem }, { .fn = c55x_x_mac_accoef }, { .fn = c55x_x_mac_acdst } } },
	{ .mask = 0xff000c00, .match = 0xd2000400, .id = TMS320C55_INS_MASM, .lop = C55_LOP_MAS, .len = 3, .no_parallel = true, .mods = 0x207, .ops = { { .lo = 8, .width = 8, .fn = c55x_x_smem }, { .fn = c55x_x_mac_accoef }, { .fn = c55x_x_mac_acdst } } },
	// the squares: the coefficient extractor collapses to NONE so the
	// two-operand (ACx == ACy) form is lifted as well -- the legacy decoder has
	// no IL for either, so the structured square is a strict improvement.
	{ .mask = 0xff000c00, .match = 0xd2000800, .id = TMS320C55_INS_SQAM, .lop = C55_LOP_MAC, .len = 3, .no_parallel = true, .mods = 0x207, .square = true, .ops = { { .lo = 8, .width = 8, .fn = c55x_x_smem }, { .fn = c55x_x_mac_accsrc }, { .fn = c55x_x_mac_acdst } } },
	{ .mask = 0xff000c00, .match = 0xd2000c00, .id = TMS320C55_INS_SQSM, .lop = C55_LOP_MAS, .len = 3, .no_parallel = true, .mods = 0x207, .square = true, .ops = { { .lo = 8, .width = 8, .fn = c55x_x_smem }, { .fn = c55x_x_mac_accsrc }, { .fn = c55x_x_mac_acdst } } },
	// mpym[r] Smem, [ACx,] ACy and mpym[r] Smem, Tx, ACx (opcode 0xd3, three
	// bytes): the no-accumulate counterparts of the d2 / d4 MACs. The last
	// byte's bits 2-3 select the form: 00 multiplies by the source accumulator's
	// high word (ACx(32-16), the ACx operand collapsing against ACy as for the d2
	// MACs but with the two-operand form lifted here), 01 multiplies by a T
	// register (bits 0-1) and 10 is the squaring sqrm[r] (ACx = Smem * Smem,
	// below). mpymu (bits 2-3 = 11) is the unsigned multiply: both operands carry
	// uns and the mnemonic gains the 'u' suffix. The legacy mis-lifted it as a
	// signed multiply; the shared path multiplies unsigned (the disassembly is
	// unchanged, so only the IL -- now correct -- differs).
	{ .mask = 0xff000c00, .match = 0xd3000000, .id = TMS320C55_INS_MPYM, .len = 3, .no_parallel = true, .mods = 0x207, .ops = { { .lo = 8, .width = 8, .fn = c55x_x_smem }, { .fn = c55x_x_mac_accsrc }, { .fn = c55x_x_mac_acdst } } },
	{ .mask = 0xff000c00, .match = 0xd3000400, .id = TMS320C55_INS_MPYM, .len = 3, .no_parallel = true, .mods = 0x207, .ops = { { .lo = 8, .width = 8, .fn = c55x_x_smem }, { .lo = 0, .fn = c55x_x_mac_tcoef }, { .fn = c55x_x_mac_acdst } } },
	// sqrm[r] Smem, ACx (op = 10): squaring multiply with no accumulation, the
	// single accumulator (bits 4-5) being the destination; the source-AC bits
	// are unused. No legacy IL, so the structured lift is a strict improvement.
	{ .mask = 0xff000c00, .match = 0xd3000800, .id = TMS320C55_INS_SQRM, .len = 3, .no_parallel = true, .mods = 0x207, .square = true, .ops = { { .lo = 8, .width = 8, .fn = c55x_x_smem }, { .fn = c55x_x_mac_acdst } } },
	{ .mask = 0xff000c00, .match = 0xd3000c00, .id = TMS320C55_INS_MPYM, .len = 3, .no_parallel = true, .mods = 0x207, .uns_all = true, .ops = { { .lo = 8, .width = 8, .fn = c55x_x_smem_u }, { .lo = 0, .fn = c55x_x_tcoef_u }, { .fn = c55x_x_mac_acdst } } },
	// macm[r] / masm[r] Smem, Tx, [ACx,] ACy (opcodes 0xd4 / 0xd5, three bytes):
	// a memory MAC whose coefficient is a T register, ACy = acc +/- sx40(Smem) *
	// sx40(Tx). The last byte holds the accumulator source ACx (bits 0-1), the
	// coefficient Tx (bits 2-3), the destination ACy (bits 4-5), round (bit 6)
	// and the side-load flag (bit 7). There is no operation field -- the opcode
	// selects add vs subtract -- so the rows pin only the opcode and uns = 0
	// (bit 15). The four-operand form (ACx != ACy) accumulates into ACx; the
	// three-operand form accumulates into ACy. The uns / side-load forms remain
	// on the legacy decoder.
	{ .mask = 0xff000000, .match = 0xd4000000, .id = TMS320C55_INS_MACM, .lop = C55_LOP_MAC, .len = 3, .no_parallel = true, .mods = 0x207, .ops = { { .lo = 8, .width = 8, .fn = c55x_x_smem }, { .lo = 2, .fn = c55x_x_mac_tcoef }, { .fn = c55x_x_mac_accsrc }, { .fn = c55x_x_mac_acdst } } },
	{ .mask = 0xff000000, .match = 0xd5000000, .id = TMS320C55_INS_MASM, .lop = C55_LOP_MAS, .len = 3, .no_parallel = true, .mods = 0x207, .ops = { { .lo = 8, .width = 8, .fn = c55x_x_smem }, { .lo = 2, .fn = c55x_x_mac_tcoef }, { .fn = c55x_x_mac_accsrc }, { .fn = c55x_x_mac_acdst } } },
	// add / sub Smem, [src,] dst (opcodes 0xd6/0xd7, three bytes): a memory
	// operand added to (or subtracted from) a register, dst = src +/- Smem. The
	// Smem byte is the middle byte; the last byte holds the destination gr4
	// register (bits 4-7) and the source gr4 register (bits 0-3), the latter
	// printed only when it differs from the destination. The opcode's low bit
	// selects add vs sub, so the leading byte is matched whole (no parallel
	// form). Like the legacy these are typed but left unlifted (the generic
	// add/sub lifter declines the memory source), and const-indexed / absolute
	// Smem modes fall through to the legacy decoder via c55x_x_smem.
	{ .mask = 0xff000000, .match = 0xd6000000, .id = TMS320C55_INS_ADD, .len = 3, .no_parallel = true, .ops = { { .lo = 8, .width = 8, .fn = c55x_x_smem }, { .lo = 0, .fn = c55x_x_gr4_src }, { .lo = 4, .fn = c55x_x_gr4 } } },
	{ .mask = 0xff000000, .match = 0xd7000000, .id = TMS320C55_INS_SUB, .len = 3, .no_parallel = true, .ops = { { .lo = 8, .width = 8, .fn = c55x_x_smem }, { .lo = 0, .fn = c55x_x_gr4_src }, { .lo = 4, .fn = c55x_x_gr4 } } },
	// sub src, Smem, dst (opcode 0xd8, three bytes): the reverse-subtract form,
	// dst = src - Smem, with the source register printed first. Same FDDD/FSSS
	// last byte and Smem middle byte as 0xd6/0xd7, but both registers are always
	// shown (no collapse). The legacy types it as a subtraction and leaves it
	// unlifted (the generic subtract lifter declines the memory minuend).
	{ .mask = 0xff000000, .match = 0xd8000000, .id = TMS320C55_INS_SUB, .len = 3, .no_parallel = true, .ops = { { .lo = 0, .fn = c55x_x_gr4 }, { .lo = 8, .width = 8, .fn = c55x_x_smem }, { .lo = 4, .fn = c55x_x_gr4 } } },
	// and / or / xor Smem, [src,] dst (opcodes 0xd9/0xda/0xdb, three bytes): the
	// bitwise counterparts of 0xd6/0xd7 with the identical operand layout. The
	// 16-bit memory operand is sign-extended to the accumulator width; the legacy
	// lifts these when both source and destination are accumulators.
	{ .mask = 0xff000000, .match = 0xd9000000, .id = TMS320C55_INS_AND, .lop = C55_LOP_ANDMEM, .len = 3, .no_parallel = true, .ops = { { .lo = 8, .width = 8, .fn = c55x_x_smem }, { .lo = 0, .fn = c55x_x_gr4 }, { .lo = 4, .fn = c55x_x_gr4 } } },
	{ .mask = 0xff000000, .match = 0xda000000, .id = TMS320C55_INS_OR, .lop = C55_LOP_ORMEM, .len = 3, .no_parallel = true, .ops = { { .lo = 8, .width = 8, .fn = c55x_x_smem }, { .lo = 0, .fn = c55x_x_gr4 }, { .lo = 4, .fn = c55x_x_gr4 } } },
	{ .mask = 0xff000000, .match = 0xdb000000, .id = TMS320C55_INS_XOR, .lop = C55_LOP_XORMEM, .len = 3, .no_parallel = true, .ops = { { .lo = 8, .width = 8, .fn = c55x_x_smem }, { .lo = 0, .fn = c55x_x_gr4 }, { .lo = 4, .fn = c55x_x_gr4 } } },
	// add / sub Smem << Tx, [ACx,] ACy (opcode 0xdd, three bytes): a memory
	// operand shifted left by the count in a T register before being added to
	// (or subtracted from) an accumulator. The Smem byte is the middle byte; the
	// last byte holds ACx (bits 6-7), ACy (bits 4-5), the shift register Tx (bits
	// 2-3), and the add/sub selector (bits 0-1: 0 add, 1 sub). ACx collapses
	// against ACy. The legacy types these and leaves them unlifted (the shifted
	// memory source is declined by the generic add/sub lifter); const-indexed
	// Smem modes fall through to the legacy decoder.
	{ .mask = 0xff000300, .match = 0xdd000000, .id = TMS320C55_INS_ADD, .len = 3, .no_parallel = true, .ops = { { .lo = 8, .width = 8, .fn = c55x_x_smem_shtx }, { .fn = c55x_x_macreg_acy_dst }, { .lo = 4, .width = 2, .param = C55_RC_AC, .fn = c55_x_reg } } },
	{ .mask = 0xff000300, .match = 0xdd000100, .id = TMS320C55_INS_SUB, .len = 3, .no_parallel = true, .ops = { { .lo = 8, .width = 8, .fn = c55x_x_smem_shtx }, { .fn = c55x_x_macreg_acy_dst }, { .lo = 4, .width = 2, .param = C55_RC_AC, .fn = c55_x_reg } } },
	// addsub2cc Smem, ACx, Tx, TC1, TC2, ACy (opcode 0xdd, variant byte2 bits
	// 0-1 = 10): conditionally add or subtract the memory word to ACx based on
	// TC1/TC2, writing ACy. The legacy types this null and leaves it unlifted;
	// OPAQUE keeps the shared disasm while declining the IL. byte2 bits 6-7 =
	// ACx, bits 4-5 = ACy, bits 2-3 = Tx; TC1/TC2 are fixed literals.
	{ .mask = 0xff000300, .match = 0xdd000200, .id = TMS320C55_INS_ADDSUB2CC, .lop = C55_LOP_OPAQUE, .len = 3, .no_parallel = true, .ops = { { .lo = 8, .width = 8, .fn = c55x_x_smem }, { .lo = 6, .width = 2, .param = C55_RC_AC, .fn = c55_x_reg }, { .lo = 2, .fn = c55x_x_t2 }, { .fn = c55x_x_tc1 }, { .fn = c55x_x_tc2 }, { .lo = 4, .width = 2, .param = C55_RC_AC, .fn = c55_x_reg } } },
	// mov [rnd(]Smem << Tx[)], ACy (opcode 0xdd, variant byte2 bits 0-1 = 11):
	// load the (Tx-shifted, optionally rounded) memory word into ACy. byte2
	// bits 4-5 = ACy, bits 2-3 = Tx, bit 6 = the rnd() wrapper. The legacy
	// leaves this unlifted (the shifted memory source is declined by the mov
	// lifter), so the IL is null.
	{ .mask = 0xff000300, .match = 0xdd000300, .id = TMS320C55_INS_MOV, .len = 3, .no_parallel = true, .ops = { { .lo = 8, .width = 8, .fn = c55x_x_smem_shtx_rnd }, { .lo = 4, .width = 2, .param = C55_RC_AC, .fn = c55_x_reg } } },
	// btst K4, Smem, TCx (opcode 0xdc, the bit-test sub-form): test bit K4 of
	// Smem and record it in the test-control flag TC1 or TC2. 0xdc is a large
	// multi-form opcode -- byte2 bit 1 selects btst (0) versus the family of
	// mov-Smem-to-special-register forms (1), and byte2 bit 0 chooses TC1 (0)
	// or TC2 (1); the bit number K4 is byte2 bits 4-7. Typed AND like the other
	// bit operations, with no IL. The mov-to-special-register variants of 0xdc
	// remain on the legacy decoder.
	{ .mask = 0xff000200, .match = 0xdc000000, .id = TMS320C55_INS_BTST, .len = 3, .no_parallel = true, .ops = { { .lo = 4, .width = 4, .fn = c55_x_imm }, { .lo = 8, .width = 8, .fn = c55x_x_smem }, { .lo = 0, .width = 1, .param = C55_RC_TC, .fn = c55_x_reg } } },
	// mov Smem, <special-reg> (opcode 0xdc, the non-btst sub-forms): load a
	// data-memory word into a special register. byte2 bits 0-1 select the group
	// (2: dp/cdp/bsa*/sp/ssp/bk*/dph/pdp; 3: csr/brc*/trn*) and bits 4-7 (4-bit
	// in group 2, 3-bit in group 3) select the register. The shared mov lifter
	// loads via c55_mem_move into the register's 16-bit view (dph/pdp truncate),
	// matching the legacy IL. Undefined register selectors fall through to the
	// legacy decoder.
	{ .mask = 0xff000300, .match = 0xdc000200, .id = TMS320C55_INS_MOV, .len = 3, .no_parallel = true, .ops = { { .lo = 8, .width = 8, .fn = c55x_x_smem }, { .param = 2, .fn = c55x_x_dc_movdst } } },
	{ .mask = 0xff000300, .match = 0xdc000300, .id = TMS320C55_INS_MOV, .len = 3, .no_parallel = true, .ops = { { .lo = 8, .width = 8, .fn = c55x_x_smem }, { .param = 3, .fn = c55x_x_dc_movdst } } },
	// btstset / btstclr / btstnot K4, Smem, TCx (opcode 0xe3): test bit K4 of
	// Smem into TC1/TC2 and then set / clear / toggle that bit in memory. 0xe3
	// is multi-form -- byte2 bits 2-3 select the operation (0 set, 1 clr, 2 not;
	// 3 is the bset/bclr/bnot register-source family that stays on the legacy
	// decoder), byte2 bit 1 selects TC1 (0) or TC2 (1), byte2 bit 0 is a
	// don't-care, and the bit number K4 is byte2 bits 4-7. All three are typed
	// AND like btst (the legacy decoder typed btstset that way but left btstclr
	// and btstnot untyped); none is lifted.
	{ .mask = 0xff000c00, .match = 0xe3000000, .id = TMS320C55_INS_BTSTSET, .len = 3, .no_parallel = true, .ops = { { .lo = 4, .width = 4, .fn = c55_x_imm }, { .lo = 8, .width = 8, .fn = c55x_x_smem }, { .lo = 1, .width = 1, .param = C55_RC_TC, .fn = c55_x_reg } } },
	{ .mask = 0xff000c00, .match = 0xe3000400, .id = TMS320C55_INS_BTSTCLR, .len = 3, .no_parallel = true, .ops = { { .lo = 4, .width = 4, .fn = c55_x_imm }, { .lo = 8, .width = 8, .fn = c55x_x_smem }, { .lo = 1, .width = 1, .param = C55_RC_TC, .fn = c55_x_reg } } },
	{ .mask = 0xff000c00, .match = 0xe3000800, .id = TMS320C55_INS_BTSTNOT, .len = 3, .no_parallel = true, .ops = { { .lo = 4, .width = 4, .fn = c55_x_imm }, { .lo = 8, .width = 8, .fn = c55x_x_smem }, { .lo = 1, .width = 1, .param = C55_RC_TC, .fn = c55_x_reg } } },
	// bset / bclr / bnot src, Smem (opcode 0xe3, the register-source forms that
	// complete the family above): set / clear / toggle the bit of Smem selected
	// by the register src. byte2 low nibble 0xc is bset, 0xd is bclr, 0xe/0xf is
	// bnot (its bit 0 is a don't-care); src is the gr4 register in byte2 bits
	// 4-7. The legacy decoder typed bset/bclr as MOV and left bnot untyped; bnot
	// is a memory bit-toggle, so it is typed XOR here. None is lifted.
	{ .mask = 0xff000f00, .match = 0xe3000c00, .id = TMS320C55_INS_BSET, .lop = C55_LOP_OPAQUE, .len = 3, .no_parallel = true, .ops = { { .lo = 4, .fn = c55x_x_gr4 }, { .lo = 8, .width = 8, .fn = c55x_x_smem } } },
	{ .mask = 0xff000f00, .match = 0xe3000d00, .id = TMS320C55_INS_BCLR, .lop = C55_LOP_OPAQUE, .len = 3, .no_parallel = true, .ops = { { .lo = 4, .fn = c55x_x_gr4 }, { .lo = 8, .width = 8, .fn = c55x_x_smem } } },
	{ .mask = 0xff000e00, .match = 0xe3000e00, .id = TMS320C55_INS_BNOT, .lop = C55_LOP_OPAQUE, .len = 3, .no_parallel = true, .ops = { { .lo = 4, .fn = c55x_x_gr4 }, { .lo = 8, .width = 8, .fn = c55x_x_smem } } },
	// btst src, Smem, TCx (opcode 0xe0): test the bit of Smem selected by the
	// register src (the bit *number* lives in the register, not an immediate as
	// in the 0xdc K4 form) and copy it into TC1/TC2. src is the gr4 register in
	// byte2 bits 4-7, the Smem field is byte1, and byte2 bit 0 chooses TC1 (0)
	// or TC2 (1) -- rendered lowercase via the condition-flag path, matching
	// this form's legacy rendering (distinct from the uppercase 0xdc/0xe3
	// forms). byte2 bits 1-3 are don't-cares. Typed AND like the other bit
	// tests; not lifted.
	{ .mask = 0xff000000, .match = 0xe0000000, .id = TMS320C55_INS_BTST, .len = 3, .no_parallel = true, .ops = { { .lo = 4, .fn = c55x_x_gr4 }, { .lo = 8, .width = 8, .fn = c55x_x_smem }, { .lo = 0, .fn = c55x_x_tcflag } } },
	// band Smem, K16, TCx (opcodes 0xf2 / 0xf3): bitwise-AND the 16-bit mask
	// K16 with Smem and copy the zero result into a test-control bit. The K16
	// is always present (bytes 2-3, a 4-byte instruction), the Smem field is
	// byte1, and the opcode LSB selects the destination flag -- 0xf2 -> TC1,
	// 0xf3 -> TC2 (so byte0 bit0 is the flag selector, not the parallel marker:
	// no_parallel). The flag prints uppercase (TC1/TC2) via the register path,
	// matching this form's legacy rendering. Typed AND; not lifted.
	{ .mask = 0xfe000000, .match = 0xf2000000, .id = TMS320C55_INS_BAND, .len = 4, .no_parallel = true, .ops = { { .lo = 16, .width = 8, .fn = c55x_x_smem }, { .lo = 0, .width = 16, .fn = c55_x_imm }, { .lo = 24, .width = 1, .param = C55_RC_TC, .fn = c55_x_reg } } },
	// mov #k, Smem store (opcodes 0xe6 #k8 / 0xfb #k16): write an immediate
	// constant into the single-data-memory operand. The immediate is the source
	// (printed first) -- byte2 for 0xe6, bytes 2-3 for 0xfb -- and the Smem field
	// is byte1. byte0 LSB is part of the opcode here, not the parallel marker
	// (no_parallel). The shared MOV lifter declines an immediate-to-memory store
	// (it only lifts reg/imm-to-reg and reg<->memory), so the IL falls back to the
	// legacy lifter (the immediate store).
	{ .mask = 0xff000000, .match = 0xe6000000, .id = TMS320C55_INS_MOV, .len = 3, .no_parallel = true, .ops = { { .lo = 0, .width = 8, .fn = c55_x_imm }, { .lo = 8, .width = 8, .fn = c55x_x_smem } } },
	{ .mask = 0xff000000, .match = 0xfb000000, .id = TMS320C55_INS_MOV, .len = 4, .no_parallel = true, .ops = { { .lo = 0, .width = 16, .fn = c55_x_imm }, { .lo = 16, .width = 8, .fn = c55x_x_smem } } },
	// and/or #k16, Smem (opcodes 0xf4 / 0xf5): bitwise-combine an unsigned
	// 16-bit constant into the single-data-memory operand in place. The
	// immediate is the source (printed first, bytes 2-3), the Smem field is
	// byte1; byte0 LSB is opcode, not the parallel marker (no_parallel). Lifted
	// as a memory read-modify-write where the Smem mode has a shared effective
	// address (see the AND/OR op-type lifter); other modes fall back.
	{ .mask = 0xff000000, .match = 0xf4000000, .id = TMS320C55_INS_AND, .len = 4, .no_parallel = true, .ops = { { .lo = 0, .width = 16, .fn = c55_x_imm }, { .lo = 16, .width = 8, .fn = c55x_x_smem } } },
	{ .mask = 0xff000000, .match = 0xf5000000, .id = TMS320C55_INS_OR, .len = 4, .no_parallel = true, .ops = { { .lo = 0, .width = 16, .fn = c55_x_imm }, { .lo = 16, .width = 8, .fn = c55x_x_smem } } },
	// addsubcc Smem, ACx, TCx, ACy (opcode 0xde, selectors 0 and 1): a
	// conditional add/subtract -- ACy = ACx +/- Smem depending on the test-
	// control flag. 0xde is multi-form; byte2 bits 1-3 == 0 selects this form,
	// with byte2 bit 0 choosing TC1 (0) or TC2 (1), ACx in byte2 bits 6-7 and
	// ACy in bits 4-5. Typed ADD (as the legacy decoder did); not lifted. The
	// remaining 0xde forms (the two-flag addsubcc, subc, the Smem<<#16 add/sub
	// forms, and addsub/subadd) stay on the legacy decoder.
	{ .mask = 0xff000e00, .match = 0xde000000, .id = TMS320C55_INS_ADDSUBCC, .len = 3, .no_parallel = true, .ops = { { .lo = 8, .width = 8, .fn = c55x_x_smem }, { .lo = 6, .width = 2, .param = C55_RC_AC, .fn = c55_x_reg }, { .lo = 0, .width = 1, .param = C55_RC_TC, .fn = c55_x_reg }, { .lo = 4, .width = 2, .param = C55_RC_AC, .fn = c55_x_reg } } },
	// addsubcc Smem, ACx, TC1, TC2, ACy (opcode 0xde, selector 2): the two-flag
	// conditional add/subtract. The selector value 2 (byte2 bits 0-3 == 0b0010)
	// conveniently has bit 0 == 0 and bit 1 == 1, so the same TC register
	// extractor reads TC1 from bit 0 and TC2 from bit 1.
	{ .mask = 0xff000f00, .match = 0xde000200, .id = TMS320C55_INS_ADDSUBCC, .len = 3, .no_parallel = true, .ops = { { .lo = 8, .width = 8, .fn = c55x_x_smem }, { .lo = 6, .width = 2, .param = C55_RC_AC, .fn = c55_x_reg }, { .lo = 0, .width = 1, .param = C55_RC_TC, .fn = c55_x_reg }, { .lo = 1, .width = 1, .param = C55_RC_TC, .fn = c55_x_reg }, { .lo = 4, .width = 2, .param = C55_RC_AC, .fn = c55_x_reg } } },
	// subc Smem, [ACx,] ACy (opcode 0xde, selector 3): the conditional-subtract
	// (subtract-with-borrow) used in division. ACx (byte2 bits 6-7) collapses
	// against ACy (bits 4-5) when equal, leaving just the destination. The
	// legacy decoder left it untyped; it is typed SUB here. The Smem source is
	// declined by the generic sub lifter, so there is no IL (as before).
	{ .mask = 0xff000f00, .match = 0xde000300, .id = TMS320C55_INS_SUBC, .len = 3, .no_parallel = true, .ops = { { .lo = 8, .width = 8, .fn = c55x_x_smem }, { .fn = c55x_x_macreg_acy_dst }, { .lo = 4, .width = 2, .param = C55_RC_AC, .fn = c55_x_reg } } },
	// add / sub Smem << #16, [ACx,] ACy (opcode 0xde, selectors 4 and 5): the
	// memory operand is shifted left by a fixed 16 (rendered "<< #16" by the
	// memory formatter via c55x_x_smem_sh16). ACx (byte2 bits 6-7) collapses
	// against ACy (bits 4-5). Typed ADD / SUB as the legacy decoder did; the
	// shifted Smem source is declined by the generic add/sub lifter, so no IL.
	{ .mask = 0xff000f00, .match = 0xde000400, .id = TMS320C55_INS_ADD, .len = 3, .no_parallel = true, .ops = { { .lo = 8, .width = 8, .fn = c55x_x_smem_sh16 }, { .fn = c55x_x_macreg_acy_dst }, { .lo = 4, .width = 2, .param = C55_RC_AC, .fn = c55_x_reg } } },
	{ .mask = 0xff000f00, .match = 0xde000500, .id = TMS320C55_INS_SUB, .len = 3, .no_parallel = true, .ops = { { .lo = 8, .width = 8, .fn = c55x_x_smem_sh16 }, { .fn = c55x_x_macreg_acy_dst }, { .lo = 4, .width = 2, .param = C55_RC_AC, .fn = c55_x_reg } } },
	// sub ACx, Smem << #16, ACy (opcode 0xde, selector 6): the reversed-operand
	// subtract (ACx minus the shifted memory). ACx (byte2 bits 6-7) is always
	// shown here. Typed SUB; no IL for the same reason.
	{ .mask = 0xff000f00, .match = 0xde000600, .id = TMS320C55_INS_SUB, .len = 3, .no_parallel = true, .ops = { { .lo = 6, .width = 2, .param = C55_RC_AC, .fn = c55_x_reg }, { .lo = 8, .width = 8, .fn = c55x_x_smem_sh16 }, { .lo = 4, .width = 2, .param = C55_RC_AC, .fn = c55_x_reg } } },
	// addsub / subadd Tx, Smem, ACx (opcode 0xde, selectors 8 and 9): the dual
	// add-and-subtract (a butterfly forming ACx +/- and -/+ Tx around Smem).
	// Tx is byte2 bits 6-7, ACx is bits 4-5. The legacy decoder left these
	// untyped; they are categorised by their leading operation (addsub -> ADD,
	// subadd -> SUB). The dual semantics are not modelled, and the generic
	// add/sub lifter declines the memory destination, so there is no IL.
	{ .mask = 0xff000f00, .match = 0xde000800, .id = TMS320C55_INS_ADDSUB, .len = 3, .no_parallel = true, .ops = { { .lo = 6, .width = 2, .param = C55_RC_T, .fn = c55_x_reg }, { .lo = 8, .width = 8, .fn = c55x_x_smem }, { .lo = 4, .width = 2, .param = C55_RC_AC, .fn = c55_x_reg } } },
	{ .mask = 0xff000f00, .match = 0xde000900, .id = TMS320C55_INS_SUBADD, .len = 3, .no_parallel = true, .ops = { { .lo = 6, .width = 2, .param = C55_RC_T, .fn = c55_x_reg }, { .lo = 8, .width = 8, .fn = c55x_x_smem }, { .lo = 4, .width = 2, .param = C55_RC_AC, .fn = c55_x_reg } } },
	// add / sub [uns(]Smem[)], [ACx,] ACy (opcode 0xdf, selectors 6 and 7): a
	// plain add/subtract of a data-memory word into an accumulator. byte2 bits
	// 1-3 pick add (6) or sub (7), byte2 bit 0 is the unsigned-memory qualifier
	// (rendered uns(...)), ACx (bits 6-7) collapses against ACy (bits 4-5).
	// Typed ADD/SUB as the legacy decoder did; the Smem source is declined by
	// the generic add/sub lifter, so there is no IL. The 0xdf high_byte /
	// low_byte / mov / carry / borrow forms remain on the legacy decoder.
	{ .mask = 0xff000e00, .match = 0xdf000c00, .id = TMS320C55_INS_ADD, .len = 3, .no_parallel = true, .ops = { { .lo = 8, .width = 8, .fn = c55x_x_smem_uns }, { .fn = c55x_x_macreg_acy_dst }, { .lo = 4, .width = 2, .param = C55_RC_AC, .fn = c55_x_reg } } },
	{ .mask = 0xff000e00, .match = 0xdf000e00, .id = TMS320C55_INS_SUB, .len = 3, .no_parallel = true, .ops = { { .lo = 8, .width = 8, .fn = c55x_x_smem_uns }, { .fn = c55x_x_macreg_acy_dst }, { .lo = 4, .width = 2, .param = C55_RC_AC, .fn = c55_x_reg } } },
	// mov [uns(]Smem[)], ACx (opcode 0xdf, selector 2): load a data-memory word
	// into an accumulator, sign-extended to 40 bits (zero-extended when the
	// uns() qualifier from byte2 bit 0 is present). ACx is byte2 bits 4-5. The
	// shared mov lifter handles the load via c55_mem_move, matching the legacy
	// sign/zero-extend IL exactly.
	{ .mask = 0xff000e00, .match = 0xdf000400, .id = TMS320C55_INS_MOV, .len = 3, .no_parallel = true, .ops = { { .lo = 8, .width = 8, .fn = c55x_x_smem_uns }, { .lo = 4, .width = 2, .param = C55_RC_AC, .fn = c55_x_reg } } },
	// mov [uns(]high_byte/low_byte(Smem)[)], ACx (opcode 0xdf, selectors 0 and
	// 1): load the high or low byte of a data-memory word into an accumulator.
	// byte2 bits 1-3 pick high_byte (0) or low_byte (1); byte2 bit 0 is the
	// unsigned qualifier and ACx is byte2 bits 4-5. Typed MOV; the byte-access
	// load is not lifted (the memory mover declines a byte_sel operand), as in
	// the legacy decoder.
	{ .mask = 0xff000e00, .match = 0xdf000000, .id = TMS320C55_INS_MOV, .len = 3, .no_parallel = true, .ops = { { .lo = 8, .width = 8, .param = 1, .fn = c55x_x_smem_byte }, { .lo = 4, .width = 2, .param = C55_RC_AC, .fn = c55_x_reg } } },
	{ .mask = 0xff000e00, .match = 0xdf000200, .id = TMS320C55_INS_MOV, .len = 3, .no_parallel = true, .ops = { { .lo = 8, .width = 8, .param = 2, .fn = c55x_x_smem_byte }, { .lo = 4, .width = 2, .param = C55_RC_AC, .fn = c55_x_reg } } },
	// add [uns(]Smem[)], CARRY, [ACx,] ACy / sub [uns(]Smem[)], BORROW, [ACx,]
	// ACy (opcode 0xdf, selectors 4 and 5): add-with-carry / subtract-with-
	// borrow of a memory word into an accumulator. byte2 bits 1-3 select add
	// CARRY (4) or sub BORROW (5); the flag operand is read from byte2 bit 1
	// (pinned by the row mask: 0 -> CARRY, 1 -> BORROW). byte2 bit 0 is the
	// unsigned qualifier and ACx (bits 6-7) collapses against ACy (bits 4-5).
	// Typed ADD/SUB as the legacy decoder did; the Smem source is declined by
	// the generic add/sub lifter, so there is no IL.
	{ .mask = 0xff000e00, .match = 0xdf000800, .id = TMS320C55_INS_ADD, .len = 3, .no_parallel = true, .ops = { { .lo = 8, .width = 8, .fn = c55x_x_smem_uns }, { .lo = 1, .width = 1, .param = C55_RC_SPECIAL, .fn = c55_x_reg }, { .fn = c55x_x_macreg_acy_dst }, { .lo = 4, .width = 2, .param = C55_RC_AC, .fn = c55_x_reg } } },
	{ .mask = 0xff000e00, .match = 0xdf000a00, .id = TMS320C55_INS_SUB, .len = 3, .no_parallel = true, .ops = { { .lo = 8, .width = 8, .fn = c55x_x_smem_uns }, { .lo = 1, .width = 1, .param = C55_RC_SPECIAL, .fn = c55_x_reg }, { .fn = c55x_x_macreg_acy_dst }, { .lo = 4, .width = 2, .param = C55_RC_AC, .fn = c55_x_reg } } },
	// mov src, dst (opcode 0x90): register-to-register move over the extended
	// "xsrc"/"xdst" register set (0-3 AC, 4-7 XSP/XSSP/XDP/XCDP, 8-15 XAR). byte1
	// bits 4-7 = src, bits 0-3 = dst; the shared MOV lifter sign/zero-extends to
	// the destination width (and declines when an operand has no lifter var).
	{ .mask = 0xff000000, .match = 0x90000000, .id = TMS320C55_INS_MOV, .len = 2, .no_parallel = true, .ops = { { .lo = 4, .fn = c55x_x_xgr4 }, { .lo = 0, .fn = c55x_x_xgr4 } } },
	// b acx (opcode 0x91) / call acx (opcode 0x92): register-indirect branch and
	// call to the address in an accumulator (AC0-3 in the low two bits).
	{ .mask = 0xff000000, .match = 0x91000000, .id = TMS320C55_INS_B, .len = 2, .ops = { { .lo = 0, .width = 2, .fn = c55x_x_ac2 } } },
	{ .mask = 0xff000000, .match = 0x92000000, .id = TMS320C55_INS_CALL, .len = 2, .ops = { { .lo = 0, .width = 2, .fn = c55x_x_ac2 } } },
	// b L16 (opcode 0x06) / call L16 (opcode 0x08): 16-bit pc-relative branch and
	// call. Three bytes, the signed displacement in bytes 1-2; target is
	// pc + size + sign-extended(disp16). Same machinery as the 0x4a short branch
	// (a reltarget immediate plus c55_effective_type refining 'b'/'call' to a
	// direct JMP/CALL), just a wider field, and both are parallel-capable.
	{ .mask = 0xfe000000, .match = 0x06000000, .id = TMS320C55_INS_B, .len = 3, .ops = { { .lo = 0, .width = 16, .fn = c55_x_imm, .param = 2 } } },
	{ .mask = 0xfe000000, .match = 0x08000000, .id = TMS320C55_INS_CALL, .len = 3, .ops = { { .lo = 0, .width = 16, .fn = c55_x_imm, .param = 2 } } },
	// b P24 (opcode 0x6a and 0x6b) / call P24 (opcode 0x6c): 24-bit absolute
	// branch and call. Four bytes, the destination address in bytes 1-3; the
	// target is that address directly (abs_target), not a pc-relative offset.
	// These are not parallel-capable: 0x6b is a second plain 'b' encoding (not
	// '|| b'), and 0x6d is a different instruction (bcc), so each pins its byte.
	// The operand is an addr (rendered as a 24-bit address) carrying abs_target.
	{ .mask = 0xff000000, .match = 0x6a000000, .id = TMS320C55_INS_B, .len = 4, .ops = { { .lo = 0, .width = 24, .fn = c55_x_imm, .param = 12 } } },
	{ .mask = 0xff000000, .match = 0x6b000000, .id = TMS320C55_INS_B, .len = 4, .ops = { { .lo = 0, .width = 24, .fn = c55_x_imm, .param = 12 } } },
	{ .mask = 0xff000000, .match = 0x6c000000, .id = TMS320C55_INS_CALL, .len = 4, .ops = { { .lo = 0, .width = 24, .fn = c55_x_imm, .param = 12 } } },
	// reset (opcode 0x94): software reset, no operands.
	{ .mask = 0xff000000, .match = 0x94000000, .id = TMS320C55_INS_RESET, .len = 2 },
	// ret / reti (opcode 0x48): within this group the low three bits of the
	// operand byte select the operation (0b100 = ret, 0b101 = reti; bits 3-7 are
	// don't-cares for these two), so the rows match byte0 plus those three bits
	// via a 0xff070000 mask. Both pop a return address and are typed as returns;
	// the remaining 0x48 sub-ops (rpt/rptadd/rptsub csr) stay on the legacy path.
	{ .mask = 0xff070000, .match = 0x48040000, .id = TMS320C55_INS_RET, .len = 2 },
	{ .mask = 0xff070000, .match = 0x48050000, .id = TMS320C55_INS_RETI, .len = 2 },
	// xcc / xccpart (opcode 0x96): predicated execution. Bit 7 of the operand
	// byte selects xccpart (set) from xcc (clear), and the low seven bits are the
	// condition field. Only register-versus-zero comparisons are decoded here;
	// status-flag conditions fall back to the legacy decoder via c55x_x_cond.
	{ .mask = 0xff800000, .match = 0x96000000, .id = TMS320C55_INS_XCC, .lop = C55_LOP_NOP, .len = 2, .ops = { { .lo = 0, .width = 7, .fn = c55x_x_cond } } },
	{ .mask = 0xff800000, .match = 0x96800000, .id = TMS320C55_INS_XCCPART, .lop = C55_LOP_NOP, .xcc_guard = true, .len = 2, .ops = { { .lo = 0, .width = 7, .fn = c55x_x_cond } } },
	// bcc short form (opcodes 0x60-0x67): the destination offset is a four-bit
	// field with the high three bits in byte0 (bits 0-2) and the low bit in byte1
	// (bit 7), at packed bits 7-10; target = pc + size + offset. Bit 0 of the
	// leading byte is part of that offset, not the parallel marker (no_parallel).
	// The remaining low seven bits of byte1 carry the same register-versus-zero
	// condition field as xcc; status-flag conditions fall back to the legacy
	// decoder via c55x_x_cond.
	{ .mask = 0xf8000000, .match = 0x60000000, .id = TMS320C55_INS_BCC, .len = 2, .no_parallel = true, .ops = { { .lo = 7, .width = 4, .fn = c55x_x_bcc_short_target }, { .lo = 0, .width = 7, .fn = c55x_x_cond } } },
	// bcc 0x04 form: a 3-byte conditional branch. byte1 carries the register-
	// versus-zero condition field (bit 7 unused) and byte2 is a signed 8-bit
	// pc-relative offset (target = pc + size + offset). Parallel-capable
	// (0x05 = || bcc); status-flag conditions fall back to the legacy decoder.
	{ .mask = 0xfe000000, .match = 0x04000000, .id = TMS320C55_INS_BCC, .len = 3, .ops = { { .lo = 0, .width = 8, .fn = c55_x_imm, .param = 2 }, { .lo = 8, .width = 7, .fn = c55x_x_cond } } },
	// bcc L16 form (opcode 0x6d): byte1 is the register-versus-zero condition,
	// byte2-3 a signed 16-bit pc-relative offset (target = pc + size + offset).
	// The legacy front-end recovered the displacement by re-parsing the rendered
	// text and sign-extending to 8 bits, truncating the L16 offset; decoding it
	// from the instruction bits computes the correct target.
	{ .mask = 0xff000000, .match = 0x6d000000, .id = TMS320C55_INS_BCC, .len = 4, .ops = { { .lo = 0, .width = 16, .fn = c55_x_imm, .param = 2 }, { .lo = 16, .width = 7, .fn = c55x_x_cond } } },
	// bcc P24 form (opcode 0x68): byte1 is the register-versus-zero condition,
	// byte2-4 a 24-bit absolute program address (same as the unconditional P24
	// branch). The legacy 8-bit text-parsing path mis-computed this too; the
	// shared decoder takes the absolute target directly from the bits.
	{ .mask = 0xff000000, .match = 0x68000000, .id = TMS320C55_INS_BCC, .len = 5, .ops = { { .lo = 0, .width = 24, .fn = c55_x_imm, .param = 12 }, { .lo = 24, .width = 7, .fn = c55x_x_cond } } },
	// bcc compare-and-branch (opcode 0x6f): byte1 selects the source register
	// (gr4) and signed comparison, byte2 is the 8-bit compare constant K8, and
	// byte3 a signed 8-bit pc-relative offset. The unsigned form (bccu) is left
	// to the legacy decoder via c55x_x_cond_imm. The legacy text-parsing path
	// sign-extended this displacement to 16 bits; decoding the L8 offset from the
	// instruction bits yields the correct (possibly negative) targets.
	{ .mask = 0xff000000, .match = 0x6f000000, .id = TMS320C55_INS_BCC, .len = 4, .ops = { { .lo = 0, .width = 8, .fn = c55_x_imm, .param = 2 }, { .lo = 8, .width = 16, .fn = c55x_x_cond_imm } } },
	// callcc (conditional call): byte1 is the register-versus-zero condition.
	// 0x6e is the L16 form (byte2-3 a signed 16-bit pc-relative offset); 0x69 is
	// the P24 form (byte2-4 a 24-bit absolute target). These reuse the same
	// target machinery as the conditional branches with a CCALL type and the
	// call stack effect. The legacy analysis read the displacement from the wrong
	// bytes (a 16-bit value starting at the condition byte), so its targets were
	// wrong for both forms; decoding from the bits computes them correctly.
	{ .mask = 0xff000000, .match = 0x6e000000, .id = TMS320C55_INS_CALLCC, .len = 4, .ops = { { .lo = 0, .width = 16, .fn = c55_x_imm, .param = 2 }, { .lo = 16, .width = 7, .fn = c55x_x_cond } } },
	{ .mask = 0xff000000, .match = 0x69000000, .id = TMS320C55_INS_CALLCC, .len = 5, .ops = { { .lo = 0, .width = 24, .fn = c55_x_imm, .param = 12 }, { .lo = 24, .width = 7, .fn = c55x_x_cond } } },
	// retcc (conditional return, opcode 0x02; 0x03 = || retcc): byte1 carries the
	// register-versus-zero condition and byte2 is unused. Typed CRET (a pop with
	// a fall-through edge); no IL. Status-flag conditions fall back to the legacy
	// decoder via c55x_x_cond.
	{ .mask = 0xfe000000, .match = 0x02000000, .id = TMS320C55_INS_RETCC, .len = 3, .ops = { { .lo = 8, .width = 7, .fn = c55x_x_cond } } },
	// rpt k8 (opcode 0x4c): repeat the next instruction (k8+1) times; the count
	// is the full operand byte. Parallel-capable, so the row matches 0x4c/0x4d.
	{ .mask = 0xfe000000, .match = 0x4c000000, .id = TMS320C55_INS_RPT, .len = 2, .ops = { { .lo = 0, .width = 8, .fn = c55_x_imm } } },
	// intr k5 / trap k5 (opcode 0x95): software interrupt vs trap to a 5-bit
	// vector. The two share the leading byte and are told apart by bit 7 of the
	// operand byte, so the rows reach into byte1 with a 0xff800000 mask; the
	// vector is the low five bits (bits 5-6 are don't-cares).
	{ .mask = 0xff800000, .match = 0x95000000, .id = TMS320C55_INS_INTR, .len = 2, .ops = { { .lo = 0, .width = 5, .fn = c55_x_imm } } },
	{ .mask = 0xff800000, .match = 0x95800000, .id = TMS320C55_INS_TRAP, .len = 2, .ops = { { .lo = 0, .width = 5, .fn = c55_x_imm } } },
	// b offset (opcode 0x4a with bit 7 of the operand byte clear; bit 7 set is
	// rptblocal below). A short pc-relative branch: the displacement is the
	// operand byte sign-extended to 8 bits (bit 7 being clear here, it is always
	// a forward 0..127). The id 'b' is shared with the register-indirect form
	// (0x91); c55_effective_type refines this immediate-operand form to a direct
	// JMP. The offset is a reltarget so it drives target computation yet renders
	// as a plain immediate rather than a 24-bit address.
	{ .mask = 0xfe800000, .match = 0x4a000000, .id = TMS320C55_INS_B, .len = 2, .ops = { { .lo = 0, .width = 8, .fn = c55_x_imm, .param = 2 } } },
	// rptblocal k7 (opcode 0x4a with bit 7 of the operand byte set; bit 7 clear
	// is the offset branch b, left to the legacy decoder). Block-local repeat
	// with a 7-bit count. Both parallel-capable and byte1-discriminated, so the
	// mask composes the two: 0xfe leaves the parallel bit free, 0x..800000 pins
	// the discriminator (matching 0x4a/0x4b with operand bit 7 set).
	{ .mask = 0xfe800000, .match = 0x4a800000, .id = TMS320C55_INS_RPTBLOCAL, .len = 2, .ops = { { .lo = 0, .width = 7, .fn = c55_x_imm } } },

	// --- dual "::" MACs (opcodes 0x82-0x85) -----------------------------
	// Two parallel sub-MACs sharing a Cmem coefficient. The leading byte picks
	// the family and byte2 bits 2-3 (packed bits 10-11) the sub-op pair; the
	// operands and sub-op metadata are produced by c55x_fill_dual. The id only
	// drives the op type (all -> MUL); the disasm uses the dual formatter.
	// 0x85 op=10 (triple amar) / op=11 (firsadd/firssub) and all of 0x86 stay
	// on the legacy front-end for now.
	// 0x82: <sub1> :: mpy
	{ .mask = 0xff000c00, .match = 0x82000000, .id = TMS320C55_INS_MPY, .len = 4, .dual = true, .lop = C55_LOP_NONE, .lop2 = C55_LOP_NONE },
	{ .mask = 0xff000c00, .match = 0x82000400, .id = TMS320C55_INS_MPY, .len = 4, .dual = true, .lop = C55_LOP_MAC, .lop2 = C55_LOP_NONE },
	{ .mask = 0xff000c00, .match = 0x82000800, .id = TMS320C55_INS_MPY, .len = 4, .dual = true, .lop = C55_LOP_MAS, .lop2 = C55_LOP_NONE },
	{ .mask = 0xff000c00, .match = 0x82000c00, .id = TMS320C55_INS_AMAR, .len = 4, .dual = true, .amar1 = true, .lop2 = C55_LOP_NONE },
	// 0x83: <sub1> :: mac
	{ .mask = 0xff000c00, .match = 0x83000000, .id = TMS320C55_INS_MAC, .len = 4, .dual = true, .lop = C55_LOP_MAC, .lop2 = C55_LOP_MAC },
	{ .mask = 0xff000c00, .match = 0x83000400, .id = TMS320C55_INS_MAC, .len = 4, .dual = true, .lop = C55_LOP_MAS, .lop2 = C55_LOP_MAC },
	{ .mask = 0xff000c00, .match = 0x83000800, .id = TMS320C55_INS_MAC, .len = 4, .dual = true, .lop = C55_LOP_MAC, .shift1 = true, .lop2 = C55_LOP_MAC },
	{ .mask = 0xff000c00, .match = 0x83000c00, .id = TMS320C55_INS_AMAR, .len = 4, .dual = true, .amar1 = true, .lop2 = C55_LOP_MAC },
	// 0x84: <sub1> :: mac >> #16
	{ .mask = 0xff000c00, .match = 0x84000000, .id = TMS320C55_INS_MAS, .len = 4, .dual = true, .lop = C55_LOP_MAS, .lop2 = C55_LOP_MAC, .shift2 = true },
	{ .mask = 0xff000c00, .match = 0x84000400, .id = TMS320C55_INS_AMAR, .len = 4, .dual = true, .amar1 = true, .lop2 = C55_LOP_MAC, .shift2 = true },
	{ .mask = 0xff000c00, .match = 0x84000800, .id = TMS320C55_INS_MAS, .len = 4, .dual = true, .lop = C55_LOP_NONE, .lop2 = C55_LOP_MAC, .shift2 = true },
	{ .mask = 0xff000c00, .match = 0x84000c00, .id = TMS320C55_INS_MAS, .len = 4, .dual = true, .lop = C55_LOP_MAC, .shift1 = true, .lop2 = C55_LOP_MAC, .shift2 = true },
	// 0x85: <sub1> :: mas (op=00 amar::mas, op=01 mas::mas)
	{ .mask = 0xff000c00, .match = 0x85000000, .id = TMS320C55_INS_AMAR, .len = 4, .dual = true, .amar1 = true, .lop2 = C55_LOP_MAS },
	{ .mask = 0xff000c00, .match = 0x85000400, .id = TMS320C55_INS_MAS, .len = 4, .dual = true, .lop = C55_LOP_MAS, .lop2 = C55_LOP_MAS },
	// 0x85 op=10: triple-register amar (amar Xmem, Ymem, Cmem) - three address
	// modifies, no product; classified LEA and lifted as the sequence of the
	// operands' post-modify side effects. (op=11 firsadd/firssub stays legacy.)
	{ .mask = 0xff000c00, .match = 0x85000800, .id = TMS320C55_INS_AMAR, .len = 4, .ops = { { .fn = c55x_x_dual_xmem }, { .fn = c55x_x_dual_ymem }, { .fn = c55x_x_dual_cmem3 } } },
	// 0x85 op=11: FIR symmetric/antisymmetric filter step. byte3 bit4 selects
	// firsadd (0) vs firssub (1); ACx is byte3 bits 2-3, ACy byte3 bits 6-7.
	{ .mask = 0xff000c10, .match = 0x85000c00, .id = TMS320C55_INS_FIRSADD, .lop = C55_LOP_FIRSADD, .len = 4, .ops = { { .fn = c55x_x_dual_xmem }, { .fn = c55x_x_dual_ymem }, { .fn = c55x_x_dual_cmem3 }, { .lo = 2, .width = 2, .fn = c55_x_reg, .param = C55_RC_AC }, { .lo = 6, .width = 2, .fn = c55_x_reg, .param = C55_RC_AC } } },
	{ .mask = 0xff000c10, .match = 0x85000c10, .id = TMS320C55_INS_FIRSSUB, .lop = C55_LOP_FIRSSUB, .len = 4, .ops = { { .fn = c55x_x_dual_xmem }, { .fn = c55x_x_dual_ymem }, { .fn = c55x_x_dual_cmem3 }, { .lo = 2, .width = 2, .fn = c55_x_reg, .param = C55_RC_AC }, { .lo = 6, .width = 2, .fn = c55_x_reg, .param = C55_RC_AC } } },
	// 0x86 dual-multiply family selected by byte3 bits 5-7: mpym (0, ACx=Xmem*Ymem),
	// macm (1, MAC), masm (3, MAS). uns()/T3=/r/40 qualifiers and the source/dest
	// accumulators are decoded by the xymac extractors and .mods (round=bit0,
	// side-load=bit1, M40=bit4). The macm/masm two-operand form (SS==DD) drops the
	// explicit source slot. (macm>>16 (2), the ::mov pairs (4,5), lms (6) and
	// sqdst/abdst (14,15) stay on the legacy path for now.)
	{ .mask = 0xff0000e0, .match = 0x86000000, .id = TMS320C55_INS_MPYM, .len = 4, .mods = 0x5081, .ops = { { .fn = c55x_x_xymac_xmem }, { .fn = c55x_x_xymac_ymem }, { .lo = 8, .width = 2, .fn = c55_x_reg, .param = C55_RC_AC } } },
	{ .mask = 0xff0000e0, .match = 0x86000020, .id = TMS320C55_INS_MACM, .lop = C55_LOP_MAC, .len = 4, .mods = 0x5081, .ops = { { .fn = c55x_x_xymac_xmem }, { .fn = c55x_x_xymac_ymem }, { .fn = c55x_x_xymac_src }, { .lo = 8, .width = 2, .fn = c55_x_reg, .param = C55_RC_AC } } },
	{ .mask = 0xff0000e0, .match = 0x86000060, .id = TMS320C55_INS_MASM, .lop = C55_LOP_MAS, .len = 4, .mods = 0x5081, .ops = { { .fn = c55x_x_xymac_xmem }, { .fn = c55x_x_xymac_ymem }, { .fn = c55x_x_xymac_src }, { .lo = 8, .width = 2, .fn = c55_x_reg, .param = C55_RC_AC } } },
	// 0x86 macm>>16 (byte3 bits 5-7 = 2): like macm but the accumulator is shifted
	// right 16 before the product (ACy = (ACx >> #16) + Xmem*Ymem).
	{ .mask = 0xff0000e0, .match = 0x86000040, .id = TMS320C55_INS_MACM, .lop = C55_LOP_MAC, .len = 4, .mods = 0x5081, .shift16 = true, .ops = { { .fn = c55x_x_xymac_xmem }, { .fn = c55x_x_xymac_ymem }, { .fn = c55x_x_xymac_src }, { .lo = 8, .width = 2, .fn = c55_x_reg, .param = C55_RC_AC } } },
	// 0x86 lms (byte3 bits 5-7 = 6): ACy += Xmem*Ymem :: ACx = round(ACx + Xmem<<16).
	// ACx is byte2 bits 0-1, ACy bits 2-3; the round/reserved last-byte low bits do
	// not affect the disassembly. (The ::mov pairs at 4,5 stay on the legacy path.)
	{ .mask = 0xff0000e0, .match = 0x860000c0, .id = TMS320C55_INS_LMS, .lop = C55_LOP_LMS, .len = 4, .ops = { { .fn = c55x_x_dual_xmem }, { .fn = c55x_x_dual_ymem }, { .lo = 8, .width = 2, .fn = c55_x_reg, .param = C55_RC_AC }, { .lo = 10, .width = 2, .fn = c55_x_reg, .param = C55_RC_AC } } },
	// 0x86 distance forms (byte3 bits 4-7: sqdst=14, abdst=15): ACy += ACx.h^2 (or
	// |ACx.h|) :: ACx = (Xmem<<16) - (Ymem<<16). ACx is byte2 bits 0-1, ACy bits 2-3.
	{ .mask = 0xff0000f0, .match = 0x860000e0, .id = TMS320C55_INS_SQDST, .lop = C55_LOP_SQDST, .len = 4, .ops = { { .fn = c55x_x_dual_xmem }, { .fn = c55x_x_dual_ymem }, { .lo = 8, .width = 2, .fn = c55_x_reg, .param = C55_RC_AC }, { .lo = 10, .width = 2, .fn = c55_x_reg, .param = C55_RC_AC } } },
	{ .mask = 0xff0000f0, .match = 0x860000f0, .id = TMS320C55_INS_ABDST, .lop = C55_LOP_ABDST, .len = 4, .ops = { { .fn = c55x_x_dual_xmem }, { .fn = c55x_x_dual_ymem }, { .lo = 8, .width = 2, .fn = c55_x_reg, .param = C55_RC_AC }, { .lo = 10, .width = 2, .fn = c55_x_reg, .param = C55_RC_AC } } },
	// 0x86 MAC :: parallel load (byte3 bits 5-7: masm::mov=4, macm::mov=5):
	//   ACx = ACx -/+ (Xmem * Tx) [, T3=Xmem] :: ACy = Ymem << #16.
	// ops: Xmem, Tx (byte3 bits 2-3), ACx (byte2 bits 0-1), Ymem, ACy (byte2 bits 2-3);
	// .mods packs round (bit0) and the T3= side-load (bit1).
	{ .mask = 0xff0000e0, .match = 0x86000080, .id = TMS320C55_INS_MASM, .lop = C55_LOP_MAS, .len = 4, .mods = 0x81, .mac_mov = true, .ops = { { .fn = c55x_x_dual_xmem }, { .lo = 2, .fn = c55x_x_mac_tcoef }, { .lo = 8, .width = 2, .fn = c55_x_reg, .param = C55_RC_AC }, { .fn = c55x_x_dual_ymem }, { .lo = 10, .width = 2, .fn = c55_x_reg, .param = C55_RC_AC } } },
	{ .mask = 0xff0000e0, .match = 0x860000a0, .id = TMS320C55_INS_MACM, .lop = C55_LOP_MAC, .len = 4, .mods = 0x81, .mac_mov = true, .ops = { { .fn = c55x_x_dual_xmem }, { .lo = 2, .fn = c55x_x_mac_tcoef }, { .lo = 8, .width = 2, .fn = c55_x_reg, .param = C55_RC_AC }, { .fn = c55x_x_dual_ymem }, { .lo = 10, .width = 2, .fn = c55_x_reg, .param = C55_RC_AC } } },
	// --- A-unit ALU aadd / amov / asub (opcode 0x14) ----------------------
	// 3 bytes. The operation and operand form are selected by the low nibble
	// of byte2: bit 2 chooses register (0) vs P8-immediate (1) source, and
	// bits[1:0] select aadd (00) / amov (01) / asub (10); bit 3 is a redundant
	// sub-bank that does not change the operation. The XAC (extended pointer)
	// register form is marked by a 1 in byte1's low nibble and matched first.
	// XACS = byte1[7:4], XACD = byte2[7:4]; FSSS/FDDD are the same fields read
	// as the AC/T/AR set; P8 = byte1. dst = dst <op> src via the shared AREG
	// lift (aadd/amov/asub all carry IL on C55x).
	{ .mask = 0xff0f0700, .match = 0x14010000, .id = TMS320C55_INS_AADD, .lop = C55_LOP_AREG_ADD, .len = 3, .ops = { { .lo = 12, .fn = c55x_x_xgr4 }, { .lo = 4, .fn = c55x_x_xgr4 } } },
	{ .mask = 0xff0f0700, .match = 0x14010100, .id = TMS320C55_INS_AMOV, .lop = C55_LOP_AREG_MOV, .len = 3, .ops = { { .lo = 12, .fn = c55x_x_xgr4 }, { .lo = 4, .fn = c55x_x_xgr4 } } },
	{ .mask = 0xff0f0700, .match = 0x14010200, .id = TMS320C55_INS_ASUB, .lop = C55_LOP_AREG_SUB, .len = 3, .ops = { { .lo = 12, .fn = c55x_x_xgr4 }, { .lo = 4, .fn = c55x_x_xgr4 } } },
	{ .mask = 0xff000700, .match = 0x14000000, .id = TMS320C55_INS_AADD, .lop = C55_LOP_AREG_ADD, .len = 3, .ops = { { .lo = 12, .fn = c55x_x_gr4 }, { .lo = 4, .fn = c55x_x_gr4 } } },
	{ .mask = 0xff000700, .match = 0x14000100, .id = TMS320C55_INS_AMOV, .lop = C55_LOP_AREG_MOV, .len = 3, .ops = { { .lo = 12, .fn = c55x_x_gr4 }, { .lo = 4, .fn = c55x_x_gr4 } } },
	{ .mask = 0xff000700, .match = 0x14000200, .id = TMS320C55_INS_ASUB, .lop = C55_LOP_AREG_SUB, .len = 3, .ops = { { .lo = 12, .fn = c55x_x_gr4 }, { .lo = 4, .fn = c55x_x_gr4 } } },
	{ .mask = 0xff000700, .match = 0x14000400, .id = TMS320C55_INS_AADD, .lop = C55_LOP_AREG_ADD, .len = 3, .ops = { { .lo = 8, .width = 8, .fn = c55_x_imm }, { .lo = 4, .fn = c55x_x_gr4 } } },
	{ .mask = 0xff000700, .match = 0x14000500, .id = TMS320C55_INS_AMOV, .lop = C55_LOP_AREG_MOV, .len = 3, .ops = { { .lo = 8, .width = 8, .fn = c55_x_imm }, { .lo = 4, .fn = c55x_x_gr4 } } },
	{ .mask = 0xff000700, .match = 0x14000600, .id = TMS320C55_INS_ASUB, .lop = C55_LOP_AREG_SUB, .len = 3, .ops = { { .lo = 8, .width = 8, .fn = c55_x_imm }, { .lo = 4, .fn = c55x_x_gr4 } } },
	// --- mov K16, dst (opcode 0x76, byte3[3:2] == 10) ---------------------
	// 4 bytes; K16 = byte1:byte2 (MSB-first), dst = byte3[7:4] (AC/T/AR). The
	// shared AREG move lifts dst = K16.
	{ .mask = 0xff00000c, .match = 0x76000008, .id = TMS320C55_INS_MOV, .lop = C55_LOP_AREG_MOV, .len = 4, .ops = { { .lo = 8, .width = 16, .fn = c55_x_imm }, { .lo = 4, .fn = c55x_x_gr4 } } },
	// --- xcc / xccpart (opcodes 0x9e / 0x9f) ------------------------------
	// As the 0x96 forms: byte1[7] selects xccpart (1) over xcc (0); the 7-bit
	// condition is byte1[6:0]. Standalone, the qualifier gates the following
	// instruction, which per-instruction lifting expresses as nop.
	{ .mask = 0xff800000, .match = 0x9e000000, .id = TMS320C55_INS_XCC, .lop = C55_LOP_NOP, .len = 2, .ops = { { .lo = 0, .width = 7, .fn = c55x_x_cond } } },
	{ .mask = 0xff800000, .match = 0x9e800000, .id = TMS320C55_INS_XCCPART, .lop = C55_LOP_NOP, .len = 2, .ops = { { .lo = 0, .width = 7, .fn = c55x_x_cond } } },
	{ .mask = 0xff800000, .match = 0x9f000000, .id = TMS320C55_INS_XCC, .lop = C55_LOP_NOP, .len = 2, .ops = { { .lo = 0, .width = 7, .fn = c55x_x_cond } } },
	{ .mask = 0xff800000, .match = 0x9f800000, .id = TMS320C55_INS_XCCPART, .lop = C55_LOP_NOP, .len = 2, .ops = { { .lo = 0, .width = 7, .fn = c55x_x_cond } } },
	// --- dual-memory move (opcode 0x80) -----------------------------------
	// 3 bytes; byte1 = XXXMMMYY, byte2 = YMMM00xx with byte2[3:2] selecting the
	// form: 00 = mov dbl(Xmem),dbl(Ymem); 01 = mov Xmem,Ymem; 10 = mov ACx,
	// Xmem,Ymem (ACx = byte2[1:0]). Xmem is the source, Ymem the destination.
	{ .mask = 0xff000c00, .match = 0x80000000, .id = TMS320C55_INS_MOV, .lop = C55_LOP_MOVMEM, .len = 3, .ops = { { .fn = c55x_x_dual_xmem3_dbl }, { .fn = c55x_x_dual_ymem3_dbl } } },
	{ .mask = 0xff000c00, .match = 0x80000400, .id = TMS320C55_INS_MOV, .lop = C55_LOP_MOVMEM, .len = 3, .ops = { { .fn = c55x_x_dual_xmem3 }, { .fn = c55x_x_dual_ymem3 } } },
	{ .mask = 0xff000c00, .match = 0x80000800, .id = TMS320C55_INS_MOV, .lop = C55_LOP_OPAQUE, .len = 3, .ops = { { .lo = 0, .width = 2, .fn = c55_x_reg, .param = C55_RC_AC }, { .fn = c55x_x_dual_xmem3 }, { .fn = c55x_x_dual_ymem3 } } },
	// --- dual-memory add / sub into ACx (opcode 0x81) ---------------------
	// Same dual-memory layout as 0x80; byte2[3:2] selects 00 = add Xmem,Ymem,
	// ACx; 01 = sub; 10 = mov Xmem,Ymem,ACx. ACx = byte2[1:0]. ACx = sx(Xmem)
	// +/- sx(Ymem).
	{ .mask = 0xff000c00, .match = 0x81000000, .id = TMS320C55_INS_ADD, .lop = C55_LOP_DUALADD, .len = 3, .ops = { { .fn = c55x_x_dual_xmem3 }, { .fn = c55x_x_dual_ymem3 }, { .lo = 0, .width = 2, .fn = c55_x_reg, .param = C55_RC_AC } } },
	{ .mask = 0xff000c00, .match = 0x81000400, .id = TMS320C55_INS_SUB, .lop = C55_LOP_DUALSUB, .len = 3, .ops = { { .fn = c55x_x_dual_xmem3 }, { .fn = c55x_x_dual_ymem3 }, { .lo = 0, .width = 2, .fn = c55_x_reg, .param = C55_RC_AC } } },
	{ .mask = 0xff000c00, .match = 0x81000800, .id = TMS320C55_INS_MOV, .lop = C55_LOP_OPAQUE, .len = 3, .ops = { { .fn = c55x_x_dual_xmem3 }, { .fn = c55x_x_dual_ymem3 }, { .lo = 0, .width = 2, .fn = c55_x_reg, .param = C55_RC_AC } } },
	// --- swap ar0, ar1 (opcode 0x5e, key 56) ------------------------------
	// The single arbitrary-AR swap pair; byte1 == 0x38 distinguishes it from
	// the 4-bit sel pairs. Lifts as the same XOR exchange as the other swaps.
	{ .mask = 0xfeff0000, .match = 0x5e380000, .id = TMS320C55_INS_SWAP, .len = 2, .ops = { { .lo = 0, .param = 0, .fn = c55x_x_swap }, { .lo = 0, .param = 1, .fn = c55x_x_swap } } },
	// --- idle (opcode 0x7a, byte3[3:1] == 6) ------------------------------
	// Low-power idle; no data effect.
	{ .mask = 0xff00000e, .match = 0x7a00000c, .id = TMS320C55_INS_IDLE, .lop = C55_LOP_OPAQUE, .len = 4 },
	// --- 0x87 parallel dual-MAC with hi-word store ------------------------
	// 4 bytes; the dual-memory Xmem/Ymem and SS/DD accumulator fields live in
	// byte1/byte2 (the 0x82-0x86 layout). byte3 selects the form: bits[7:5] =
	// 000/001/010 -> mpym/macm/masm "[t3=]Xmem, Tx, ACy :: mov hi(ACx << t2),
	// Ymem" (Tx = byte3[3:2], round = byte3[0]); = 100/101/110 -> add/sub/mov
	// "Xmem << #16, ... :: mov hi(ACz << t2), Ymem"; byte3 == 0x61 -> lmsf
	// Xmem, Ymem, ACx, ACy. All decode-only (no modelled IL).
	{ .mask = 0xff0000ff, .match = 0x87000061, .id = TMS320C55_INS_LMSF, .lop = C55_LOP_OPAQUE, .len = 4, .ops = { { .fn = c55x_x_dual_xmem }, { .fn = c55x_x_dual_ymem }, { .lo = 8, .width = 2, .fn = c55_x_reg, .param = C55_RC_AC }, { .lo = 10, .width = 2, .fn = c55_x_reg, .param = C55_RC_AC } } },
	{ .mask = 0xff0000e0, .match = 0x87000000, .id = TMS320C55_INS_MPYM, .lop = C55_LOP_OPAQUE, .len = 4, .mods = 0x01, .mac_store = true, .ops = { { .fn = c55x_x_dual_xmem }, { .lo = 2, .width = 2, .fn = c55x_x_t2 }, { .lo = 8, .width = 2, .fn = c55_x_reg, .param = C55_RC_AC }, { .fn = c55x_x_dual_ymem }, { .lo = 10, .width = 2, .fn = c55_x_reg, .param = C55_RC_AC } } },
	{ .mask = 0xff0000e0, .match = 0x87000020, .id = TMS320C55_INS_MACM, .lop = C55_LOP_OPAQUE, .len = 4, .mods = 0x01, .mac_store = true, .ops = { { .fn = c55x_x_dual_xmem }, { .lo = 2, .width = 2, .fn = c55x_x_t2 }, { .lo = 8, .width = 2, .fn = c55_x_reg, .param = C55_RC_AC }, { .fn = c55x_x_dual_ymem }, { .lo = 10, .width = 2, .fn = c55_x_reg, .param = C55_RC_AC } } },
	{ .mask = 0xff0000e0, .match = 0x87000040, .id = TMS320C55_INS_MASM, .lop = C55_LOP_OPAQUE, .len = 4, .mods = 0x01, .mac_store = true, .ops = { { .fn = c55x_x_dual_xmem }, { .lo = 2, .width = 2, .fn = c55x_x_t2 }, { .lo = 8, .width = 2, .fn = c55_x_reg, .param = C55_RC_AC }, { .fn = c55x_x_dual_ymem }, { .lo = 10, .width = 2, .fn = c55_x_reg, .param = C55_RC_AC } } },
	{ .mask = 0xff0000e0, .match = 0x87000080, .id = TMS320C55_INS_ADD, .lop = C55_LOP_OPAQUE, .len = 4, .mac_store = true, .ops = { { .fn = c55x_x_dual_xmem }, { .lo = 10, .width = 2, .fn = c55_x_reg, .param = C55_RC_AC }, { .lo = 8, .width = 2, .fn = c55_x_reg, .param = C55_RC_AC }, { .fn = c55x_x_dual_ymem } } },
	{ .mask = 0xff0000e0, .match = 0x870000a0, .id = TMS320C55_INS_SUB, .lop = C55_LOP_OPAQUE, .len = 4, .mac_store = true, .ops = { { .fn = c55x_x_dual_xmem }, { .lo = 10, .width = 2, .fn = c55_x_reg, .param = C55_RC_AC }, { .lo = 8, .width = 2, .fn = c55_x_reg, .param = C55_RC_AC }, { .fn = c55x_x_dual_ymem } } },
	{ .mask = 0xff0000e0, .match = 0x870000c0, .id = TMS320C55_INS_MOV, .lop = C55_LOP_OPAQUE, .len = 4, .mac_store = true, .ops = { { .fn = c55x_x_dual_xmem }, { .lo = 8, .width = 2, .fn = c55_x_reg, .param = C55_RC_AC }, { .fn = c55x_x_dual_ymem }, { .lo = 10, .width = 2, .fn = c55_x_reg, .param = C55_RC_AC } } },
	// --- mant ACx, ACy :: nexp ACx, Tx (opcode 0x10, selector 9) ----------
	// 3 bytes; ACx = byte1[5:4] (SS), ACy = byte1[7:6] (DD), Tx = byte2[5:4]
	// (dd). ACx is shared by both halves. Decode-only.
	{ .mask = 0xff0f0000, .match = 0x10090000, .id = TMS320C55_INS_MANT, .lop = C55_LOP_OPAQUE, .len = 3, .mant_nexp = true, .ops = { { .lo = 12, .width = 2, .fn = c55_x_reg, .param = C55_RC_AC }, { .lo = 14, .width = 2, .fn = c55_x_reg, .param = C55_RC_AC }, { .lo = 4, .width = 2, .fn = c55x_x_t2 } } },
	// --- addsub / subadd Tx, dual(Lmem), ACy (opcode 0xee, sel 6/7) -------
	// 3 bytes; same Lmem (AAAAAAAI) layout as the 0xee add/sub forms. The
	// SS field is the Tx operand and DD the ACy. byte2[3:1] selects the form.
	{ .mask = 0xff000e00, .match = 0xee000c00, .id = TMS320C55_INS_ADDSUB, .lop = C55_LOP_OPAQUE, .len = 3, .ops = { { .lo = 6, .width = 2, .fn = c55x_x_t2 }, { .lo = 8, .fn = c55x_x_smem_dual }, { .lo = 4, .width = 2, .fn = c55_x_reg, .param = C55_RC_AC } } },
	{ .mask = 0xff000e00, .match = 0xee000e00, .id = TMS320C55_INS_SUBADD, .lop = C55_LOP_OPAQUE, .len = 3, .ops = { { .lo = 6, .width = 2, .fn = c55x_x_t2 }, { .lo = 8, .fn = c55x_x_smem_dual }, { .lo = 4, .width = 2, .fn = c55_x_reg, .param = C55_RC_AC } } },
	// --- mpymk / macmk [t3=]Smem, K8, [ACx,] ACy (opcode 0xf8) ------------
	// 4 bytes; Smem = byte1, K8 = byte2. byte3[2] selects macmk (1) over mpymk
	// (0); byte3[0] is the rounding (r) bit and byte3[1] the T3= side-load (the
	// legacy "U" field). mpymk has only ACx = byte3[5:4]; macmk adds ACx =
	// byte3[7:6] (SS) with ACy = byte3[5:4] (DD). Decode-only.
	{ .mask = 0xff000004, .match = 0xf8000000, .id = TMS320C55_INS_MPYMK, .lop = C55_LOP_OPAQUE, .len = 4, .mods = 0x81, .ops = { { .lo = 16, .fn = c55x_x_smem }, { .lo = 8, .width = 8, .fn = c55_x_imm }, { .lo = 4, .width = 2, .fn = c55_x_reg, .param = C55_RC_AC } } },
	{ .mask = 0xff000004, .match = 0xf8000004, .id = TMS320C55_INS_MACMK, .lop = C55_LOP_OPAQUE, .len = 4, .mods = 0x81, .ops = { { .lo = 16, .fn = c55x_x_smem }, { .lo = 8, .width = 8, .fn = c55_x_imm }, { .lo = 6, .width = 2, .fn = c55_x_reg, .param = C55_RC_AC }, { .lo = 4, .width = 2, .fn = c55_x_reg, .param = C55_RC_AC } } },
	// --- ret (opcode 0x48, selector 4) -----------------------------------
	// 2 bytes; selector = byte1[2:0] (4 = ret). byte0 bit0 is the parallel (E)
	// flag, so masking it off (0xfe) lets the shared engine raise the "|| "
	// prefix when set (the "|| ret" form 0x49 0x04).
	{ .mask = 0xfe0f0000, .match = 0x48040000, .id = TMS320C55_INS_RET, .lop = C55_LOP_NOP, .len = 2 },
	// --- band Smem, k16, TC1 (opcode 0xf2) -------------------------------
	// 4 bytes; Smem = byte1 (CDP modes 0x71/0x91/... now decode to *cdp...),
	// k16 = bytes 2-3 (the low 16 bits of the packed word). TC1 is a fixed
	// literal operand. Decode-only.
	{ .mask = 0xff000000, .match = 0xf2000000, .id = TMS320C55_INS_BAND, .lop = C55_LOP_OPAQUE, .len = 4, .ops = { { .lo = 16, .fn = c55x_x_smem }, { .lo = 0, .width = 16, .fn = c55_x_imm }, { .fn = c55x_x_tc1 } } },
	// --- btstp Baddr, src (opcode 0xec, selector 2) ----------------------
	// 3 bytes; selector = byte2[3:1] (2 = btstp). src = byte2[7:4] (FSSS). The
	// Baddr bit-address is not decoded by the legacy and renders as the literal
	// "Baddr". Decode-only.
	{ .mask = 0xff000e00, .match = 0xec000400, .id = TMS320C55_INS_BTSTP, .lop = C55_LOP_OPAQUE, .len = 3, .ops = { { .fn = c55x_x_baddr }, { .lo = 4, .fn = c55x_x_gr4 } } },
};

const C55ArchDesc c55x_arch_desc = {
	.arch = C55_ARCH_C55X,
	.cpu_name = "c55x",
	.table = c55x_table,
	.table_len = sizeof(c55x_table) / sizeof(c55x_table[0]),
	.insn_len = c55x_insn_len,
	.reg_info = c55x_reg_info,
	.mnemonic = c55x_mnemonic,
	.op_type = c55x_op_type,
	.lift = NULL,
	.mem = { .addr_unit_log2 = 1, .ptr_width = 23, .big_endian = false, .page_reg = "dph" },
	.ea = NULL,
	.fill_dual = c55x_fill_dual,
};

int tms320_c55x_op_byte(RzAnalysis *analysis, RzAnalysisOp *op, ut64 addr,
	const ut8 *buf, int len, RzAnalysisOpMask mask) {
	if (!op || !buf || len < 1) {
		return 0;
	}

	op->addr = addr;
	op->type = RZ_ANALYSIS_OP_TYPE_NULL;

	/* Decode-once analysis: the C55x instruction is decoded a single time by
	 * the shared decode-IR engine, and the analysis op (type, branch targets,
	 * basic-block fall-through, src/dst/val, stack effects, instruction id) and
	 * the RzIL lift are both derived from that one decoded C55Insn. Anything the
	 * engine does not decode is reported as an illegal instruction. */
	C55Insn ci;
	if (c55_decode(&c55x_arch_desc, buf, len, &ci)) {
		c55_fill_analysis(&c55x_arch_desc, &ci, op);
		if (mask & RZ_ANALYSIS_OP_MASK_IL) {
			op->il_op = c55_lift(&c55x_arch_desc, &ci, op->addr);
		}
		return op->size;
	}

	op->type = RZ_ANALYSIS_OP_TYPE_ILL;
	op->size = 1;
	return op->size;
}
