mirror of
https://github.com/entropykit/entropia
synced 2026-06-24 06:05:04 +00:00
369 lines
12 KiB
Rust
369 lines
12 KiB
Rust
|
|
use crate::encoder::{Reg64, Segment};
|
|
|
|
// AST
|
|
#[derive(Debug, Clone)]
|
|
pub enum AsmOperand {
|
|
Reg(Reg64),
|
|
Imm(i64),
|
|
/// %name - resolved at codegen against the enclosing scope.
|
|
Sym(String),
|
|
Mem(AsmMem),
|
|
}
|
|
|
|
/// [base ± index*scale ± disp]. Components are optional.
|
|
#[derive(Debug, Clone)]
|
|
pub struct AsmMem {
|
|
/// Register or %name (mutually exclusive in V1).
|
|
pub base: Option<AsmMemBase>,
|
|
/// (register, scale). Scale ∈ {1,2,4,8}.
|
|
pub index: Option<(Reg64, u32)>,
|
|
pub disp: i32,
|
|
/// gs: / fs: override (None = flat segment).
|
|
pub seg: Option<Segment>,
|
|
}
|
|
|
|
#[derive(Debug, Clone)]
|
|
pub enum AsmMemBase {
|
|
Reg(Reg64),
|
|
Sym(String),
|
|
}
|
|
|
|
/// Mnemonic is a string so the AST doesn't enumerate every variant;
|
|
/// codegen dispatches per-mnemonic.
|
|
#[derive(Debug, Clone)]
|
|
pub enum AsmBody {
|
|
Op0 { mnem: String },
|
|
Op1 { mnem: String, op: AsmOperand },
|
|
Op2 { mnem: String, dst: AsmOperand, src: AsmOperand },
|
|
Label (String),
|
|
Db (Vec<u8>),
|
|
}
|
|
|
|
/// Parsed asm line with source position for debug breadcrumbs.
|
|
/// line == 0 is "unknown" (synthetic / pre-parsed).
|
|
#[derive(Debug, Clone)]
|
|
pub struct AsmLine {
|
|
pub line: u32,
|
|
pub col: u32,
|
|
pub body: AsmBody,
|
|
}
|
|
|
|
impl AsmLine {
|
|
#[allow(dead_code)]
|
|
pub fn from_body(body: AsmBody) -> Self {
|
|
Self { line: 0, col: 0, body }
|
|
}
|
|
}
|
|
|
|
|
|
// PARSER
|
|
|
|
/// Parse one asm line. Forms: name:, db 0x..[, 0x..]*, or
|
|
/// mnem [operand[, operand]]. Empty lines are rejected.
|
|
pub fn parse_asm_line(line: &str) -> Result<AsmBody, String> {
|
|
let trimmed = strip_trailing_comment(line).trim();
|
|
if trimmed.is_empty() { return Err("empty asm line".into()); }
|
|
|
|
// Label declaration: name: or name :
|
|
if let Some(name) = parse_label_decl(trimmed) {
|
|
return Ok(AsmBody::Label(name));
|
|
}
|
|
|
|
// db 0x.., 0x..
|
|
if let Some(rest) = trimmed.strip_prefix("db ").or_else(|| trimmed.strip_prefix("db\t")) {
|
|
return parse_db(rest);
|
|
}
|
|
|
|
// Generic mnemonic + comma-separated operands.
|
|
let (mnem, rest) = split_mnemonic(trimmed);
|
|
let operands = if rest.is_empty() {
|
|
Vec::new()
|
|
} else {
|
|
split_top_level_commas(rest)?
|
|
.iter()
|
|
.map(|s| parse_operand(s.trim()))
|
|
.collect::<Result<Vec<_>, _>>()?
|
|
};
|
|
|
|
let mnem = mnem.to_ascii_lowercase();
|
|
match operands.len() {
|
|
0 => Ok(AsmBody::Op0 { mnem }),
|
|
1 => Ok(AsmBody::Op1 { mnem, op: operands.into_iter().next().unwrap() }),
|
|
2 => {
|
|
let mut it = operands.into_iter();
|
|
let dst = it.next().unwrap();
|
|
let src = it.next().unwrap();
|
|
Ok(AsmBody::Op2 { mnem, dst, src })
|
|
}
|
|
n => Err(format!("`{mnem}` takes 0/1/2 operands, got {n}")),
|
|
}
|
|
}
|
|
|
|
/// Strip // ... from EOL. The host parser already removes //
|
|
/// from source-level tokens; this catches comments appended to
|
|
/// individual asm lines.
|
|
fn strip_trailing_comment(line: &str) -> &str {
|
|
if let Some(idx) = line.find("//") { return &line[..idx]; }
|
|
line
|
|
}
|
|
|
|
/// name: -> bare label name. Anything else -> None.
|
|
fn parse_label_decl(s: &str) -> Option<String> {
|
|
let body = s.strip_suffix(':')?;
|
|
let name = body.trim();
|
|
if name.is_empty() { return None; }
|
|
validate_ident(name).ok()?;
|
|
Some(name.to_string())
|
|
}
|
|
|
|
/// (mnemonic, rest). Mnemonic ends at the first whitespace.
|
|
fn split_mnemonic(s: &str) -> (&str, &str) {
|
|
let bytes = s.as_bytes();
|
|
let mut i = 0;
|
|
while i < bytes.len() && !(bytes[i] as char).is_ascii_whitespace() {
|
|
i += 1;
|
|
}
|
|
let mnem = &s[..i];
|
|
let rest = s[i..].trim_start();
|
|
(mnem, rest)
|
|
}
|
|
|
|
/// Comma-split, but respect [ ... ] so [rbp + 4*rax] doesn't tear.
|
|
fn split_top_level_commas(s: &str) -> Result<Vec<&str>, String> {
|
|
let mut out = Vec::new();
|
|
let mut depth = 0;
|
|
let mut start = 0;
|
|
let bytes = s.as_bytes();
|
|
for (i, &b) in bytes.iter().enumerate() {
|
|
match b {
|
|
b'[' | b'(' => depth += 1,
|
|
b']' | b')' => {
|
|
if depth == 0 { return Err("unmatched `]` / `)` in asm operand".into()); }
|
|
depth -= 1;
|
|
}
|
|
b',' if depth == 0 => {
|
|
out.push(&s[start..i]);
|
|
start = i + 1;
|
|
}
|
|
_ => {}
|
|
}
|
|
}
|
|
if depth != 0 { return Err("unmatched `[` / `(` in asm operand".into()); }
|
|
out.push(&s[start..]);
|
|
Ok(out)
|
|
}
|
|
|
|
fn parse_db(rest: &str) -> Result<AsmBody, String> {
|
|
let mut bytes = Vec::new();
|
|
for piece in rest.split(',') {
|
|
let p = piece.trim();
|
|
let val: i64 = parse_int_literal(p)?;
|
|
if !(0..=0xFF).contains(&val) {
|
|
return Err(format!("db value out of range: {p}"));
|
|
}
|
|
bytes.push(val as u8);
|
|
}
|
|
Ok(AsmBody::Db(bytes))
|
|
}
|
|
|
|
/// Decimal / hex / negative integer literal. Hex needs 0x prefix.
|
|
fn parse_int_literal(s: &str) -> Result<i64, String> {
|
|
let s = s.trim();
|
|
if let Some(neg) = s.strip_prefix('-') {
|
|
let v = parse_int_literal(neg)?;
|
|
return Ok(v.wrapping_neg());
|
|
}
|
|
if let Some(h) = s.strip_prefix("0x").or_else(|| s.strip_prefix("0X")) {
|
|
return u64::from_str_radix(h, 16)
|
|
.map(|v| v as i64)
|
|
.map_err(|e| format!("bad hex literal `{s}`: {e}"));
|
|
}
|
|
s.parse::<i64>().map_err(|e| format!("bad integer `{s}`: {e}"))
|
|
}
|
|
|
|
/// Routes by leading char: [ mem, % sym, digit/- imm, else reg.
|
|
fn parse_operand(s: &str) -> Result<AsmOperand, String> {
|
|
let s = s.trim();
|
|
if s.is_empty() { return Err("empty operand".into()); }
|
|
if s.starts_with('[') {
|
|
return parse_mem_operand(s, None);
|
|
}
|
|
// gs:[...] / fs:[...]
|
|
if let Some(rest) = strip_seg_prefix(s) {
|
|
let (seg, body) = rest;
|
|
if !body.starts_with('[') {
|
|
return Err(format!(
|
|
"asm: segment override `{}:` must be followed by `[ ... ]`",
|
|
match seg { Segment::Fs => "fs", Segment::Gs => "gs" }
|
|
));
|
|
}
|
|
return parse_mem_operand(body, Some(seg));
|
|
}
|
|
if let Some(name) = s.strip_prefix('%') {
|
|
let name = name.trim();
|
|
if name.is_empty() {
|
|
return Err("`%` operand: missing name".into());
|
|
}
|
|
validate_ident(name)?;
|
|
return Ok(AsmOperand::Sym(name.to_string()));
|
|
}
|
|
if s.starts_with('-') || s.as_bytes()[0].is_ascii_digit() {
|
|
return Ok(AsmOperand::Imm(parse_int_literal(s)?));
|
|
}
|
|
// Register name - strictly lowercase to match Reg64::from_name.
|
|
let lower = s.to_ascii_lowercase();
|
|
if let Some(r) = Reg64::from_name(&lower) {
|
|
return Ok(AsmOperand::Reg(r));
|
|
}
|
|
// Bare identifier - equivalent to %name. The % prefix is
|
|
// available for names that could collide with a register.
|
|
if validate_ident(s).is_ok() {
|
|
return Ok(AsmOperand::Sym(s.to_string()));
|
|
}
|
|
Err(format!("unknown operand `{s}` - expected register, immediate, %name, [mem], or label"))
|
|
}
|
|
|
|
/// Strip a fs: / gs: prefix (case-insensitive). Returns
|
|
/// (seg, rest) or None.
|
|
fn strip_seg_prefix(s: &str) -> Option<(Segment, &str)> {
|
|
let bytes = s.as_bytes();
|
|
if bytes.len() < 3 || bytes[2] != b':' { return None; }
|
|
let head = &s[..2];
|
|
let seg = match head {
|
|
"fs" | "Fs" | "fS" | "FS" => Segment::Fs,
|
|
"gs" | "Gs" | "gS" | "GS" => Segment::Gs,
|
|
_ => return None,
|
|
};
|
|
Some((seg, s[3..].trim_start()))
|
|
}
|
|
|
|
/// [reg], [reg ± imm], [reg + reg[*N]], [reg + reg*N ± imm],
|
|
/// [%name], [%name ± imm]. Scale ∈ {1,2,4,8}.
|
|
fn parse_mem_operand(s: &str, seg: Option<Segment>) -> Result<AsmOperand, String> {
|
|
let body = s.strip_prefix('[')
|
|
.and_then(|r| r.strip_suffix(']'))
|
|
.ok_or_else(|| format!("memory operand must be `[ ... ]` (got `{s}`)"))?
|
|
.trim();
|
|
if body.is_empty() {
|
|
return Err("empty `[]` memory operand".into());
|
|
}
|
|
|
|
let mut mem = AsmMem { base: None, index: None, disp: 0, seg };
|
|
// Walk +/- separated terms. First term is implicitly positive.
|
|
let mut cursor = 0usize;
|
|
let bytes = body.as_bytes();
|
|
let mut sign: i64 = 1;
|
|
let mut first = true;
|
|
while cursor < bytes.len() {
|
|
while cursor < bytes.len() && (bytes[cursor] as char).is_ascii_whitespace() {
|
|
cursor += 1;
|
|
}
|
|
if cursor >= bytes.len() { break; }
|
|
if !first {
|
|
match bytes[cursor] {
|
|
b'+' => { sign = 1; cursor += 1; }
|
|
b'-' => { sign = -1; cursor += 1; }
|
|
_ => return Err(format!(
|
|
"mem operand: expected `+` or `-` between terms in `[{body}]`"
|
|
)),
|
|
}
|
|
while cursor < bytes.len() && (bytes[cursor] as char).is_ascii_whitespace() {
|
|
cursor += 1;
|
|
}
|
|
}
|
|
first = false;
|
|
|
|
// Term ends at the next +/- at the top level.
|
|
let term_start = cursor;
|
|
while cursor < bytes.len() && bytes[cursor] != b'+' && bytes[cursor] != b'-' {
|
|
cursor += 1;
|
|
}
|
|
let term = body[term_start..cursor].trim();
|
|
if term.is_empty() {
|
|
return Err(format!("mem operand: empty term in `[{body}]`"));
|
|
}
|
|
apply_mem_term(&mut mem, term, sign, body)?;
|
|
}
|
|
|
|
Ok(AsmOperand::Mem(mem))
|
|
}
|
|
|
|
/// Fold one term: reg -> base, reg*N -> index, imm -> disp,
|
|
/// %name -> sym base.
|
|
fn apply_mem_term(mem: &mut AsmMem, term: &str, sign: i64, full: &str) -> Result<(), String> {
|
|
// Scaled index: reg * N
|
|
if let Some(star) = term.find('*') {
|
|
let left = term[..star].trim();
|
|
let right = term[star + 1..].trim();
|
|
let reg = Reg64::from_name(&left.to_ascii_lowercase())
|
|
.ok_or_else(|| format!("mem operand: `{left}*…` left side must be a register"))?;
|
|
let scale = parse_int_literal(right)? as u32;
|
|
if !matches!(scale, 1 | 2 | 4 | 8) {
|
|
return Err(format!(
|
|
"mem operand: scale must be 1/2/4/8 (got {scale}) in `[{full}]`"
|
|
));
|
|
}
|
|
if mem.index.is_some() {
|
|
return Err(format!("mem operand: multiple index terms in `[{full}]`"));
|
|
}
|
|
mem.index = Some((reg, scale));
|
|
return Ok(());
|
|
}
|
|
// %name
|
|
if let Some(name) = term.strip_prefix('%') {
|
|
let name = name.trim();
|
|
validate_ident(name)?;
|
|
if mem.base.is_some() {
|
|
return Err(format!("mem operand: two bases in `[{full}]`"));
|
|
}
|
|
mem.base = Some(AsmMemBase::Sym(name.to_string()));
|
|
return Ok(());
|
|
}
|
|
// Register
|
|
let lower = term.to_ascii_lowercase();
|
|
if let Some(reg) = Reg64::from_name(&lower) {
|
|
if mem.base.is_some() {
|
|
// Second register becomes the index (scale 1).
|
|
if mem.index.is_some() {
|
|
return Err(format!(
|
|
"mem operand: too many register terms in `[{full}]`"
|
|
));
|
|
}
|
|
mem.index = Some((reg, 1));
|
|
} else {
|
|
mem.base = Some(AsmMemBase::Reg(reg));
|
|
}
|
|
return Ok(());
|
|
}
|
|
// Otherwise: integer literal contributing to disp.
|
|
let v = parse_int_literal(term)?;
|
|
let signed = (sign as i64).wrapping_mul(v);
|
|
let new_disp = (mem.disp as i64).wrapping_add(signed);
|
|
if new_disp < i32::MIN as i64 || new_disp > i32::MAX as i64 {
|
|
return Err(format!("mem operand: disp out of range in `[{full}]`"));
|
|
}
|
|
mem.disp = new_disp as i32;
|
|
Ok(())
|
|
}
|
|
|
|
/// [A-Za-z_][A-Za-z0-9_]*.
|
|
fn validate_ident(s: &str) -> Result<(), String> {
|
|
let bytes = s.as_bytes();
|
|
if bytes.is_empty() {
|
|
return Err("empty identifier".into());
|
|
}
|
|
let first = bytes[0] as char;
|
|
if !(first.is_ascii_alphabetic() || first == '_') {
|
|
return Err(format!("identifier `{s}` must start with letter or `_`"));
|
|
}
|
|
for &b in &bytes[1..] {
|
|
let c = b as char;
|
|
if !(c.is_ascii_alphanumeric() || c == '_') {
|
|
return Err(format!("identifier `{s}` has invalid char `{c}`"));
|
|
}
|
|
}
|
|
Ok(())
|
|
}
|
|
|