use std::borrow::Cow;
use crate::operand::{Constraint, OperandDesc};
use crate::regs::{PhysReg, Segment};
use crate::x86_64::insts::{ALIGN, form};
use crate::x86_64::text::{Arg, Shape, Width, gpr_named, machine, written};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Piece {
Operand {
index: usize,
width: Width,
stated: bool,
},
Implicit {
reg: PhysReg,
},
Reg {
reg: PhysReg,
width: Width,
},
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Disp {
Number(i32),
Operand(usize),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct At {
pub segment: Option<Segment>,
pub disp: Disp,
pub base: Option<Piece>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Line {
pub opcode: &'static str,
pub operands: Vec<Piece>,
pub at: Option<At>,
pub imm: Option<i64>,
}
#[must_use]
pub fn read(template: &str, widths: &[Option<Width>]) -> Option<Vec<Line>> {
let mut lines = Vec::new();
let mut carried = false;
for text in template.split(['\n', ';']) {
let text = uncommented(text).trim();
if text.is_empty() {
continue;
}
if is_repeat(text) {
if carried {
return None;
}
carried = true;
continue;
}
if let Some(line) = alignment(text) {
if carried {
return None;
}
lines.push(line);
continue;
}
lines.push(instruction(text, carried, widths)?);
carried = false;
}
if carried { None } else { Some(lines) }
}
fn alignment(text: &str) -> Option<Line> {
let (name, rest) = text.split_once(char::is_whitespace)?;
let rest = rest.trim();
if rest.contains(',') {
return None;
}
let number: u32 = rest.parse().ok()?;
let bytes = match name {
".p2align" => 1u32.checked_shl(number).filter(|&bytes| bytes <= MOST)?,
".align" | ".balign" => number,
_ => return None,
};
if !bytes.is_power_of_two() || bytes > MOST {
return None;
}
Some(Line { opcode: ALIGN, operands: Vec::new(), at: None, imm: Some(i64::from(bytes)) })
}
const MOST: u32 = 4096;
fn is_repeat(text: &str) -> bool {
matches!(text.trim(), "rep" | "repe" | "repz")
}
fn repeated(mnemonic: &str, rest: &str) -> Option<String> {
if mnemonic == "nop" && rest.trim().is_empty() {
return Some("pause".to_owned());
}
for (search, count) in [("bsf", "tzcnt"), ("bsr", "lzcnt")] {
let Some(suffix) = mnemonic.strip_prefix(search) else { continue };
if suffix.is_empty() || suffix == "l" || suffix == "q" {
return Some(format!("{count}{suffix}"));
}
}
None
}
fn uncommented(text: &str) -> &str {
let end = text.find('#').into_iter().chain(text.find("//")).min();
end.map_or(text, |at| &text[..at])
}
fn instruction(text: &str, prefixed: bool, widths: &[Option<Width>]) -> Option<Line> {
let (mnemonic, rest) = text.split_once(char::is_whitespace).unwrap_or((text, ""));
if is_repeat(mnemonic) {
if prefixed {
return None;
}
return instruction(rest.trim(), true, widths);
}
let mnemonic: Cow<'_, str> =
if prefixed { Cow::Owned(repeated(mnemonic, rest)?) } else { Cow::Borrowed(mnemonic) };
let mnemonic = mnemonic.as_ref();
if mnemonic.is_empty() || !mnemonic.chars().all(|c| c.is_ascii_alphanumeric()) {
return None;
}
let given: Vec<Given> =
arguments(rest).iter().map(|text| given(text)).collect::<Option<_>>()?;
let shapes: Vec<Shape> = given.iter().map(Given::shape).collect();
let opcode = match machine(mnemonic, &shapes) {
Some(opcode) => opcode,
None => machine(&suffixed(mnemonic, &given, widths)?, &shapes)?,
};
let [only] = written(opcode)? else { return None };
let described = form(opcode)?.operands();
let mut operands = vec![None; described.len()];
let mut at = None;
let mut imm = None;
for (&arg, &given) in only.args.iter().zip(&given) {
match (arg, given) {
(Arg::Reg(index, width), Given::Operand(operand, stated)) => {
if stated.is_some_and(|stated| stated != width) {
return None;
}
*operands.get_mut(usize::from(index))? =
Some(Piece::Operand { index: operand, width, stated: stated.is_some() });
}
(Arg::Reg(index, width), Given::Reg(reg, spelled)) => {
if spelled != width {
return None;
}
*operands.get_mut(usize::from(index))? = Some(Piece::Reg { reg, width });
}
(Arg::Imm, Given::Imm(value)) => imm = Some(value),
(Arg::Mem, Given::Mem(address)) => at = Some(address),
_ => return None,
}
}
for (slot, desc) in operands.iter_mut().zip(described) {
if slot.is_some() {
continue;
}
if let Constraint::Fixed(reg) = desc.constraint {
*slot = Some(Piece::Implicit { reg });
}
}
for index in 0..operands.len() {
if operands[index].is_some() {
continue;
}
let partner = tied(described, index)?;
operands[index] = *operands.get(partner)?;
}
let operands: Vec<Piece> = operands.into_iter().collect::<Option<_>>()?;
Some(Line { opcode, operands, at, imm })
}
fn tied(described: &[OperandDesc], index: usize) -> Option<usize> {
if let Constraint::Reuse(other) = described.get(index)?.constraint {
return Some(usize::from(other));
}
described.iter().position(
|desc| matches!(desc.constraint, Constraint::Reuse(back) if usize::from(back) == index),
)
}
fn suffixed(mnemonic: &str, given: &[Given], widths: &[Option<Width>]) -> Option<String> {
let mut width: Option<Width> = None;
for arg in given {
let each = match *arg {
Given::Reg(_, each) => each,
Given::Operand(index, stated) => match stated {
Some(stated) => stated,
None => (*widths.get(index)?)?,
},
Given::Imm(_) | Given::Mem(_) => continue,
};
if width.replace(each).is_some_and(|before| before != each) {
return None;
}
}
let suffix = match width? {
Width::Byte => 'b',
Width::Word => 'w',
Width::Long => 'l',
Width::Quad => 'q',
};
Some(format!("{mnemonic}{suffix}"))
}
fn arguments(text: &str) -> Vec<&str> {
let text = text.trim();
if text.is_empty() {
return Vec::new();
}
let mut args = Vec::new();
let mut depth = 0usize;
let mut start = 0;
for (at, letter) in text.char_indices() {
match letter {
'(' => depth += 1,
')' => depth = depth.saturating_sub(1),
',' if depth == 0 => {
args.push(text[start..at].trim());
start = at + 1;
}
_ => {}
}
}
args.push(text[start..].trim());
args
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Given {
Operand(usize, Option<Width>),
Reg(PhysReg, Width),
Imm(i64),
Mem(At),
}
impl Given {
fn shape(&self) -> Shape {
match self {
Given::Operand(..) | Given::Reg(..) => Shape::Reg,
Given::Imm(_) => Shape::Imm,
Given::Mem(_) => Shape::Mem,
}
}
}
fn given(text: &str) -> Option<Given> {
if let Some(written) = text.strip_prefix('$') {
return number(written.trim()).map(Given::Imm);
}
let Some(after) = sigil(text) else { return address(text, None).map(Given::Mem) };
if after.starts_with(|letter: char| letter.is_ascii_digit()) {
return after.parse().ok().map(|index| Given::Operand(index, None));
}
if after.starts_with('c') && text.contains('(') {
return address(text, None).map(Given::Mem);
}
if let Some(given) = modified(after) {
return Some(given);
}
if let Some((name, rest)) = after.split_once(':') {
let segment = match name {
"fs" => Segment::Fs,
"gs" => Segment::Gs,
_ => return None,
};
return address(rest, Some(segment)).map(Given::Mem);
}
let (reg, width) = gpr_named(after)?;
Some(Given::Reg(reg, width))
}
fn modified(after: &str) -> Option<Given> {
let (letter, digits) = after.split_at_checked(1)?;
let width = match letter {
"b" => Width::Byte,
"w" => Width::Word,
"k" => Width::Long,
"q" => Width::Quad,
_ => return None,
};
Some(Given::Operand(digits.parse().ok()?, Some(width)))
}
fn sigil(text: &str) -> Option<&str> {
let after = text.strip_prefix('%')?;
Some(after.strip_prefix('%').unwrap_or(after))
}
fn address(text: &str, segment: Option<Segment>) -> Option<At> {
let (front, inside) = match text.trim().split_once('(') {
Some((front, rest)) => (front.trim(), Some(rest.strip_suffix(')')?.trim())),
None => (text.trim(), None),
};
let disp = if front.is_empty() { Disp::Number(0) } else { displacement(front)? };
let base = match inside {
Some(inside) => Some(base(inside)?),
None => None,
};
Some(At { segment, disp, base })
}
fn displacement(text: &str) -> Option<Disp> {
if let Some(after) = text.strip_prefix("%c") {
return after.parse().ok().map(Disp::Operand);
}
i32::try_from(number(text)?).ok().map(Disp::Number)
}
fn base(text: &str) -> Option<Piece> {
let after = sigil(text)?;
if after.starts_with(|letter: char| letter.is_ascii_digit()) {
return after.parse().ok().map(|index| Piece::Operand {
index,
width: Width::Quad,
stated: true,
});
}
let (reg, width) = gpr_named(after)?;
(width == Width::Quad).then_some(Piece::Reg { reg, width })
}
fn number(text: &str) -> Option<i64> {
let (negative, digits) = match text.strip_prefix('-') {
Some(rest) => (true, rest.trim()),
None => (false, text.strip_prefix('+').unwrap_or(text).trim()),
};
let value = match digits.strip_prefix("0x").or_else(|| digits.strip_prefix("0X")) {
Some(hex) => i64::from_str_radix(hex, 16).ok()?,
None => digits.parse::<i64>().ok()?,
};
Some(if negative { -value } else { value })
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_template_that_is_one_mnemonic_is_the_instruction_of_that_name() {
let lines = read("pause", &[]).expect("pause is an instruction");
assert_eq!(lines.len(), 1);
assert_eq!(lines[0].opcode, "pause");
assert!(lines[0].operands.is_empty());
assert_eq!(lines[0].at, None);
}
#[test]
fn a_read_through_a_segment_is_the_load_the_machine_already_has() {
let lines = read("movq %%fs:0, %0", &[]).expect("a load through a segment");
assert_eq!(lines.len(), 1);
assert_eq!(lines[0].opcode, "mov_rm_64");
let out = Piece::Operand { index: 0, width: Width::Quad, stated: false };
assert_eq!(lines[0].operands, vec![out]);
let at = At { segment: Some(Segment::Fs), disp: Disp::Number(0), base: None };
assert_eq!(lines[0].at, Some(at));
}
#[test]
fn a_copy_puts_its_source_and_destination_where_the_opcode_holds_them() {
let lines = read("movq %1, %0", &[]).expect("a copy between two operands");
assert_eq!(lines[0].opcode, "mov_rr_64");
assert_eq!(
lines[0].operands,
vec![
Piece::Operand { index: 0, width: Width::Quad, stated: false },
Piece::Operand { index: 1, width: Width::Quad, stated: false },
]
);
}
#[test]
fn a_register_the_template_named_is_read_at_the_width_its_name_says() {
let lines = read("movq %%rax, %0", &[]).expect("a copy out of a named register");
let source = Piece::Reg { reg: PhysReg::new(0), width: Width::Quad };
assert_eq!(lines[0].operands[1], source);
assert_eq!(read("movq %%eax, %0", &[]), None, "a narrow name in a wide instruction");
}
#[test]
fn a_template_with_several_instructions_is_several_instructions() {
let lines = read("pause\n\tpause ; pause", &[]).expect("three of them");
assert_eq!(lines.len(), 3);
assert!(lines.iter().all(|line| line.opcode == "pause"));
}
#[test]
fn a_repeat_prefix_on_a_nop_is_the_spin_hint() {
for template in ["rep; nop", "rep nop", "rep\n\tnop", "repz; nop", "repe nop"] {
let lines =
read(template, &[]).unwrap_or_else(|| panic!("{template} is the spin hint"));
assert_eq!(lines.len(), 1, "{template}");
assert_eq!(lines[0].opcode, "pause", "{template}");
assert!(lines[0].operands.is_empty(), "{template}");
}
}
#[test]
fn an_instruction_whose_operands_are_all_implicit_is_read_from_the_description() {
let lines = read("cpuid", &[]).expect("cpuid is an instruction");
assert_eq!(lines.len(), 1);
assert_eq!(lines[0].opcode, "cpuid");
let regs: Vec<PhysReg> = lines[0]
.operands
.iter()
.map(|piece| match *piece {
Piece::Implicit { reg } => reg,
other => panic!("{other:?} is a piece the text named"),
})
.collect();
let expected = ["rax", "rbx", "rcx", "rdx", "rax", "rcx"];
let expected: Vec<PhysReg> =
expected.iter().map(|name| gpr_named(name).expect("a register").0).collect();
assert_eq!(regs, expected, "four written and then the two read");
}
#[test]
fn an_operand_tied_to_a_named_one_is_read_as_that_one() {
let lines = read("addq %1, %0", &[]).expect("an addition onto an operand");
assert_eq!(lines[0].opcode, "add_rr_64");
let destination = Piece::Operand { index: 0, width: Width::Quad, stated: false };
let source = Piece::Operand { index: 1, width: Width::Quad, stated: false };
assert_eq!(
lines[0].operands,
vec![destination, destination, source],
"written, read, and the other source"
);
}
#[test]
fn a_shift_by_cl_has_one_operand_of_each_kind_filled_in() {
let lines = read("shlq %%cl, %0", &[]).expect("a shift by cl");
assert_eq!(lines[0].opcode, "shl_rcl_64");
let destination = Piece::Operand { index: 0, width: Width::Quad, stated: false };
assert_eq!(lines[0].operands[0], destination);
assert_eq!(lines[0].operands[1], destination, "the value being shifted");
assert_eq!(
lines[0].operands[2],
Piece::Reg { reg: gpr_named("cl").expect("cl").0, width: Width::Byte }
);
}
#[test]
fn a_comparison_and_a_conditional_move_are_the_two_instructions_they_say_they_are() {
let widths = [Some(Width::Long); 4];
let lines = read("cmp %1, %2\ncmova %3, %0", &widths).expect("the branchless select");
assert_eq!(lines.len(), 2);
assert_eq!(lines[0].opcode, "cmp_rr_32");
assert_eq!(lines[1].opcode, "cmov_a_32");
let kept = Piece::Operand { index: 0, width: Width::Long, stated: false };
let arm = Piece::Operand { index: 3, width: Width::Long, stated: false };
assert_eq!(
lines[1].operands,
vec![kept, kept, arm],
"the destination is also the arm taken when the condition does not hold"
);
}
#[test]
fn an_operand_carrying_a_width_is_read_at_the_width_it_carries() {
let cases = [
("addb %1, %b0", "add_rr_8", Width::Byte),
("addw %1, %w0", "add_rr_16", Width::Word),
("addl %1, %k0", "add_rr_32", Width::Long),
("addq %1, %q0", "add_rr_64", Width::Quad),
];
for (template, opcode, width) in cases {
let widths = [Some(width); 2];
let lines =
read(template, &widths).unwrap_or_else(|| panic!("{template} is an addition"));
assert_eq!(lines[0].opcode, opcode, "{template}");
let written = Piece::Operand { index: 0, width, stated: true };
assert_eq!(lines[0].operands[0], written, "{template}");
}
}
#[test]
fn a_width_written_on_an_operand_is_what_the_suffix_is_worked_out_from() {
let lines = read("add %q1, %q0", &[Some(Width::Long); 2]).expect("an addition");
assert_eq!(lines[0].opcode, "add_rr_64", "the modifier and not the type");
assert_eq!(
read("add %1, %0", &[Some(Width::Long); 2]).expect("an addition")[0].opcode,
"add_rr_32",
"the type, for the same template without one"
);
let lines = read("addq %1, %q0", &[None, None]).expect("an addition");
assert_eq!(lines[0].opcode, "add_rr_64", "an operand whose type has no width here");
assert_eq!(
read("add %1, %q0", &[Some(Width::Long); 2]),
None,
"one operand saying a width and the other saying a different one"
);
}
#[test]
fn an_operand_whose_width_came_from_the_template_says_where_it_came_from() {
let widths = [Some(Width::Long), Some(Width::Quad)];
let lines = read("rep;bsf\t%1, %q0", &widths).expect("the count gmp writes");
assert_eq!(lines.len(), 1);
assert_eq!(lines[0].opcode, "tzcnt_64", "the modifier and not the type");
let written = Piece::Operand { index: 0, width: Width::Quad, stated: true };
assert_eq!(lines[0].operands[0], written, "the count, written by the whole instruction");
let read_from = Piece::Operand { index: 1, width: Width::Quad, stated: false };
assert_eq!(lines[0].operands[1], read_from, "the limb, whose own type said sixty four");
}
#[test]
fn a_width_that_disagrees_with_the_instruction_is_refused() {
assert_eq!(read("addq %1, %k0", &[Some(Width::Quad); 2]), None, "half a destination");
assert_eq!(read("addl %1, %q0", &[Some(Width::Long); 2]), None, "the other way round");
assert_eq!(read("addq %1, %h0", &[Some(Width::Quad); 2]), None, "a letter this leaves out");
assert_eq!(
read("addq %1, %q", &[Some(Width::Quad); 2]),
None,
"a modifier with no operand"
);
let lines = read("addb %%bl, %0", &[Some(Width::Byte)]).expect("an addition out of bl");
assert_eq!(
lines[0].operands[2],
Piece::Reg { reg: gpr_named("bl").expect("bl").0, width: Width::Byte },
"a register whose name starts with a letter a modifier also uses"
);
}
#[test]
fn a_mnemonic_with_no_suffix_is_refused_when_the_arguments_do_not_say_the_width() {
let mixed = [Some(Width::Long), Some(Width::Quad)];
assert_eq!(read("cmp %0, %1", &mixed), None, "two operands of different widths");
assert_eq!(read("cmp $1, $2", &[]), None, "nothing that has a width at all");
assert_eq!(
read("cmp %0, %1", &[Some(Width::Long)]),
None,
"an operand the statement has not got"
);
}
#[test]
fn a_repeat_prefix_on_a_bit_search_is_the_count_it_is_the_old_spelling_of() {
let cases = [
("bsf\t%1, %q0", "bsf_64"),
("bsr\t%1,%0", "bsr_64"),
("rep;bsf\t%1, %q0", "tzcnt_64"),
("rep;bsr\t%1, %q0", "lzcnt_64"),
("rep bsfl %1, %0", "tzcnt_32"),
("rep\n\tbsr %k1, %k0", "lzcnt_32"),
];
for (template, opcode) in cases {
let widths = [Some(Width::Quad); 2];
let lines = read(template, &widths).unwrap_or_else(|| panic!("{template} is a search"));
assert_eq!(lines.len(), 1, "{template}");
assert_eq!(lines[0].opcode, opcode, "{template}");
}
assert_eq!(
read("rep; bsfw %1, %0", &[Some(Width::Word); 2]),
None,
"the sixteen bit count, which this assembler has no encoding for"
);
}
#[test]
fn a_prefix_this_does_not_read_is_refused_rather_than_dropped() {
assert_eq!(read("lock; incl %0", &[]), None, "a lock prefix");
assert_eq!(read("rep; movsb", &[]), None, "a repeat this has no instruction for");
assert_eq!(
read("rep; pause", &[]),
None,
"a prefix on an instruction that is already the pair"
);
assert_eq!(read("rep", &[]), None, "a prefix with nothing behind it");
assert_eq!(read("rep; rep; nop", &[]), None, "two prefixes");
assert_eq!(read("rep nop, %0", &[]), None, "a prefix on an instruction with an argument");
}
#[test]
fn a_multiply_or_a_divide_on_a_pair_of_registers_reads_back_as_the_one_instruction_it_is() {
let cases = [
("mulq %3", "mul_wide_64", 4),
("imull %3", "imul_wide_32", 4),
("divq %4", "div_wide_64", 5),
("idivw %4", "idiv_wide_16", 5),
];
for (template, opcode, operands) in cases {
let widths = [Some(Width::Quad); 5];
let lines = read(template, &widths).unwrap_or_else(|| panic!("{template} is a pair"));
assert_eq!(lines.len(), 1, "{template} is one instruction");
assert_eq!(lines[0].opcode, opcode, "{template}");
assert_eq!(lines[0].operands.len(), operands, "{template}");
}
}
#[test]
fn what_cannot_be_placed_is_refused_rather_than_guessed_at() {
assert_eq!(read("hcf", &[]), None, "a mnemonic this machine does not have");
assert_eq!(read("movq %0", &[]), None, "an instruction with the wrong number of arguments");
assert_eq!(read("idivb %0", &[]), None, "an opcode the machine writes as more than one");
assert_eq!(read("again:", &[]), None, "a label");
assert_eq!(read(".byte 0", &[]), None, "a directive");
assert_eq!(read("movq (%%rax,%%rbx,8), %0", &[]), None, "a scaled index");
assert_eq!(read("movq %%cs:0, %0", &[]), None, "a segment nothing here reaches");
assert_eq!(read("movq %%xmm0, %0", &[]), None, "a register in the other file");
}
#[test]
fn a_template_with_nothing_in_it_is_no_instructions() {
assert_eq!(read("", &[]), Some(Vec::new()));
assert_eq!(read(" \n\t # nothing here \n", &[]), Some(Vec::new()));
}
#[test]
fn a_displacement_is_read_in_both_spellings_and_both_signs() {
let cases = [
("-8(%%rbp)", Disp::Number(-8)),
("0x10(%%rbp)", Disp::Number(16)),
("+4(%%rbp)", Disp::Number(4)),
("(%%rbp)", Disp::Number(0)),
("%c1(%%rbp)", Disp::Operand(1)),
];
for (written, disp) in cases {
let text = format!("movq {written}, %0");
let lines = read(&text, &[]).unwrap_or_else(|| panic!("{text} is a load"));
assert_eq!(lines[0].at.expect("an address").disp, disp, "{text}");
}
assert_eq!(read("movq %c(%%rbp), %0", &[]), None, "a modifier with no operand");
}
#[test]
fn an_alignment_is_read_in_all_three_spellings_and_carries_its_boundary_in_bytes() {
let cases = [(".p2align 5", 32), (".align 16", 16), (".balign 8", 8), (".p2align 0", 1)];
for (template, bytes) in cases {
let lines = read(template, &[]).unwrap_or_else(|| panic!("{template} is an alignment"));
assert_eq!(lines.len(), 1, "{template}");
assert_eq!(lines[0].opcode, ALIGN, "{template}");
assert!(lines[0].operands.is_empty(), "{template}");
assert_eq!(lines[0].at, None, "{template}");
assert_eq!(lines[0].imm, Some(bytes), "{template}");
}
let lines = read(".p2align 4\n\tpause", &[]).expect("an alignment in front of one");
assert_eq!(lines.len(), 2);
assert_eq!(lines[0].opcode, ALIGN);
assert_eq!(lines[1].opcode, "pause");
}
#[test]
fn an_alignment_that_asks_for_more_than_a_boundary_is_refused() {
assert_eq!(read(".p2align 4, 0x90", &[]), None, "a fill byte");
assert_eq!(read(".p2align 4, 0x90, 8", &[]), None, "a most to skip");
assert_eq!(read(".align 24", &[]), None, "a boundary that is not a power of two");
assert_eq!(read(".balign 8192", &[]), None, "a boundary larger than a page");
assert_eq!(read(".p2align 20", &[]), None, "a power larger than a page");
assert_eq!(read(".p2align", &[]), None, "a boundary that was left out");
assert_eq!(read(".p2align four", &[]), None, "a boundary that is not a number");
assert_eq!(read(".skip 16", &[]), None, "a directive that is not an alignment");
assert_eq!(read("rep; .p2align 4", &[]), None, "a prefix in front of one");
}
}