use crate::regs::{PhysReg, Segment};
use crate::x86_64::insts::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,
},
Reg {
reg: PhysReg,
width: Width,
},
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct At {
pub segment: Option<Segment>,
pub disp: i32,
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) -> Option<Vec<Line>> {
let mut lines = Vec::new();
for text in template.split(['\n', ';']) {
let text = uncommented(text).trim();
if text.is_empty() {
continue;
}
lines.push(instruction(text)?);
}
Some(lines)
}
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) -> Option<Line> {
let (mnemonic, rest) = text.split_once(char::is_whitespace).unwrap_or((text, ""));
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 = machine(mnemonic, &shapes)?;
let [only] = written(opcode)? else { return None };
let mut operands = vec![None; form(opcode)?.operands().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)) => {
*operands.get_mut(usize::from(index))? =
Some(Piece::Operand { index: operand, width });
}
(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,
}
}
let operands: Vec<Piece> = operands.into_iter().collect::<Option<_>>()?;
Some(Line { opcode, operands, at, imm })
}
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),
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(Given::Operand);
}
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 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() { 0 } else { i32::try_from(number(front)?).ok()? };
let base = match inside {
Some(inside) => Some(base(inside)?),
None => None,
};
Some(At { segment, disp, base })
}
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 });
}
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");
assert_eq!(lines[0].operands, vec![Piece::Operand { index: 0, width: Width::Quad }]);
assert_eq!(lines[0].at, Some(At { segment: Some(Segment::Fs), disp: 0, base: None }));
}
#[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 },
Piece::Operand { index: 1, width: Width::Quad },
]
);
}
#[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 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("addq %1, %0"),
None,
"an opcode with an operand its spelling does not name"
);
assert_eq!(read("idivq %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)", -8), ("0x10(%%rbp)", 16), ("+4(%%rbp)", 4)];
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}");
}
}
}