#![allow(clippy::too_many_lines, clippy::match_same_arms, dead_code)]
use std::fmt::Write;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum BitWidth {
Bits32,
Bits64,
}
impl BitWidth {
fn mask(self) -> u64 {
match self {
Self::Bits32 => 0xFFFF_FFFF,
Self::Bits64 => u64::MAX,
}
}
}
#[must_use]
pub fn simplify_text(text: &str, width: BitWidth) -> String {
let tokens = tokenize(text);
let mut parser = Parser::new(&tokens);
let Some(expr) = parser.parse_expr_or_comma_list() else {
return text.to_string();
};
if !parser.is_done() {
return text.to_string();
}
let simplified = simplify_expr(expr, width);
render(&simplified)
}
#[derive(Debug, Clone, PartialEq, Eq)]
enum Tok {
Ident(String),
Hex(u64),
Dec(u64),
Str(String),
LParen,
RParen,
LBracket,
RBracket,
Comma,
Op(&'static str),
}
fn tokenize(text: &str) -> Vec<Tok> {
let mut out = Vec::new();
let bytes = text.as_bytes();
let mut i = 0;
while i < bytes.len() {
let c = bytes[i];
if c.is_ascii_whitespace() {
i += 1;
continue;
}
if i + 1 < bytes.len() {
let two = &bytes[i..i + 2];
match two {
b"<<" => {
out.push(Tok::Op("<<"));
i += 2;
continue;
}
b">>" => {
out.push(Tok::Op(">>"));
i += 2;
continue;
}
b"==" => {
out.push(Tok::Op("=="));
i += 2;
continue;
}
b"!=" => {
out.push(Tok::Op("!="));
i += 2;
continue;
}
b"<=" => {
out.push(Tok::Op("<="));
i += 2;
continue;
}
b">=" => {
out.push(Tok::Op(">="));
i += 2;
continue;
}
b"&&" => {
out.push(Tok::Op("&&"));
i += 2;
continue;
}
b"||" => {
out.push(Tok::Op("||"));
i += 2;
continue;
}
_ => {}
}
}
match c {
b'(' => {
out.push(Tok::LParen);
i += 1;
continue;
}
b')' => {
out.push(Tok::RParen);
i += 1;
continue;
}
b'[' => {
out.push(Tok::LBracket);
i += 1;
continue;
}
b']' => {
out.push(Tok::RBracket);
i += 1;
continue;
}
b',' => {
out.push(Tok::Comma);
i += 1;
continue;
}
b'"' => {
i += 1;
let mut s = String::new();
let mut esc = false;
while i < bytes.len() {
let b = bytes[i];
if esc {
let resolved = match b {
b'n' => '\n',
b'r' => '\r',
b't' => '\t',
b'\\' => '\\',
b'"' => '"',
other => other as char,
};
s.push(resolved);
esc = false;
i += 1;
} else if b == b'\\' {
esc = true;
i += 1;
} else if b == b'"' {
i += 1;
break;
} else {
s.push(b as char);
i += 1;
}
}
out.push(Tok::Str(s));
continue;
}
b'+' | b'-' | b'*' | b'/' | b'%' | b'&' | b'|' | b'^' | b'~' | b'!' | b'<' | b'>'
| b'=' => {
let ch = c as char;
let s: &'static str = match ch {
'+' => "+",
'-' => "-",
'*' => "*",
'/' => "/",
'%' => "%",
'&' => "&",
'|' => "|",
'^' => "^",
'~' => "~",
'!' => "!",
'<' => "<",
'>' => ">",
'=' => "=",
_ => unreachable!(),
};
out.push(Tok::Op(s));
i += 1;
continue;
}
_ => {}
}
if c.is_ascii_digit() {
let start = i;
let prefixed_hex = bytes[i] == b'0'
&& (i + 1 < bytes.len() && (bytes[i + 1] == b'x' || bytes[i + 1] == b'X'));
if prefixed_hex {
i += 2;
let digits_start = i;
while i < bytes.len() && bytes[i].is_ascii_hexdigit() {
i += 1;
}
if i == digits_start {
return Vec::new();
}
let raw = std::str::from_utf8(&bytes[digits_start..i]).unwrap_or("0");
let n = u64::from_str_radix(raw, 16).unwrap_or(0);
out.push(Tok::Hex(n));
continue;
}
while i < bytes.len() && bytes[i].is_ascii_hexdigit() {
i += 1;
}
if i < bytes.len() && (bytes[i] == b'h' || bytes[i] == b'H') {
let raw = std::str::from_utf8(&bytes[start..i]).unwrap_or("0");
let n = u64::from_str_radix(raw, 16).unwrap_or(0);
out.push(Tok::Hex(n));
i += 1;
continue;
}
let chunk = &bytes[start..i];
if chunk.iter().all(u8::is_ascii_digit) {
let raw = std::str::from_utf8(chunk).unwrap_or("0");
let n: u64 = raw.parse().unwrap_or(0);
out.push(Tok::Dec(n));
continue;
}
return Vec::new();
}
if c.is_ascii_alphabetic() || c == b'_' || c == b'.' || c == b'@' {
let start = i;
while i < bytes.len() {
let b = bytes[i];
if b.is_ascii_alphanumeric() || b == b'_' || b == b'.' || b == b'@' {
i += 1;
} else {
break;
}
}
let raw = std::str::from_utf8(&bytes[start..i])
.unwrap_or("")
.to_string();
out.push(Tok::Ident(raw));
continue;
}
return Vec::new();
}
out
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Expr {
Lit(u64),
Ident(String),
StringRef(String),
Deref(Box<Expr>),
Unary(&'static str, Box<Expr>),
Binary(&'static str, Box<Expr>, Box<Expr>),
Call(String, Vec<Expr>),
CommaList(Vec<Expr>),
Raw(String),
}
struct Parser<'a> {
toks: &'a [Tok],
pos: usize,
}
impl<'a> Parser<'a> {
fn new(toks: &'a [Tok]) -> Self {
Self { toks, pos: 0 }
}
fn is_done(&self) -> bool {
self.pos >= self.toks.len()
}
fn peek(&self) -> Option<&Tok> {
self.toks.get(self.pos)
}
fn bump(&mut self) -> Option<&Tok> {
let t = self.toks.get(self.pos);
self.pos += 1;
t
}
fn eat(&mut self, t: &Tok) -> bool {
if self.peek() == Some(t) {
self.pos += 1;
true
} else {
false
}
}
fn parse_expr_or_comma_list(&mut self) -> Option<Expr> {
let first = self.parse_expr()?;
if self.peek() != Some(&Tok::Comma) {
return Some(first);
}
let mut items = vec![first];
while self.eat(&Tok::Comma) {
items.push(self.parse_expr()?);
}
Some(Expr::CommaList(items))
}
fn parse_expr(&mut self) -> Option<Expr> {
self.parse_logical_or()
}
fn parse_logical_or(&mut self) -> Option<Expr> {
self.binop_left(Self::parse_logical_and, &["||"])
}
fn parse_logical_and(&mut self) -> Option<Expr> {
self.binop_left(Self::parse_compare, &["&&"])
}
fn parse_compare(&mut self) -> Option<Expr> {
self.binop_left(Self::parse_bitwise_or, &["==", "!=", "<", "<=", ">", ">="])
}
fn parse_bitwise_or(&mut self) -> Option<Expr> {
self.binop_left(Self::parse_bitwise_xor, &["|"])
}
fn parse_bitwise_xor(&mut self) -> Option<Expr> {
self.binop_left(Self::parse_bitwise_and, &["^"])
}
fn parse_bitwise_and(&mut self) -> Option<Expr> {
self.binop_left(Self::parse_shift, &["&"])
}
fn parse_shift(&mut self) -> Option<Expr> {
self.binop_left(Self::parse_additive, &["<<", ">>"])
}
fn parse_additive(&mut self) -> Option<Expr> {
self.binop_left(Self::parse_multiplicative, &["+", "-"])
}
fn parse_multiplicative(&mut self) -> Option<Expr> {
self.binop_left(Self::parse_unary, &["*", "/", "%"])
}
fn binop_left(
&mut self,
mut next: impl FnMut(&mut Self) -> Option<Expr>,
ops: &[&'static str],
) -> Option<Expr> {
let mut lhs = next(self)?;
while let Some(op) = self.peek_op_in(ops) {
self.pos += 1;
let rhs = next(self)?;
lhs = Expr::Binary(op, Box::new(lhs), Box::new(rhs));
}
Some(lhs)
}
fn peek_op_in(&self, ops: &[&'static str]) -> Option<&'static str> {
if let Some(Tok::Op(o)) = self.peek() {
for &cand in ops {
if cand == *o {
return Some(cand);
}
}
}
None
}
fn parse_unary(&mut self) -> Option<Expr> {
if let Some(Tok::Op(o)) = self.peek() {
let op = *o;
if op == "-" || op == "~" || op == "!" || op == "&" {
self.pos += 1;
let inner = self.parse_unary()?;
let kind: &'static str = match op {
"-" => "-",
"~" => "~",
"!" => "!",
"&" => "&",
_ => unreachable!(),
};
return Some(Expr::Unary(kind, Box::new(inner)));
}
}
self.parse_postfix()
}
fn parse_postfix(&mut self) -> Option<Expr> {
self.parse_primary()
}
fn parse_primary(&mut self) -> Option<Expr> {
let tok = self.peek()?.clone();
match tok {
Tok::LParen => {
self.bump();
let inner = self.parse_expr()?;
if !self.eat(&Tok::RParen) {
return None;
}
Some(inner)
}
Tok::LBracket => {
self.bump();
let inner = self.parse_expr()?;
if !self.eat(&Tok::RBracket) {
return None;
}
Some(Expr::Deref(Box::new(inner)))
}
Tok::Hex(n) | Tok::Dec(n) => {
self.bump();
Some(Expr::Lit(n))
}
Tok::Str(s) => {
self.bump();
Some(Expr::StringRef(s))
}
Tok::Ident(name) => {
self.bump();
if self.peek() == Some(&Tok::LParen) {
self.bump();
let mut args = Vec::new();
if self.peek() != Some(&Tok::RParen) {
args.push(self.parse_expr()?);
while self.eat(&Tok::Comma) {
args.push(self.parse_expr()?);
}
}
if !self.eat(&Tok::RParen) {
return None;
}
Some(Expr::Call(name, args))
} else {
Some(Expr::Ident(name))
}
}
_ => None,
}
}
}
#[must_use]
pub fn resolve_strings(expr: Expr, lookup: &dyn Fn(u64) -> Option<String>) -> Expr {
resolve_strings_inner(expr, lookup, false)
}
fn resolve_strings_inner(
expr: Expr,
lookup: &dyn Fn(u64) -> Option<String>,
in_offset_context: bool,
) -> Expr {
match expr {
Expr::Lit(n) => {
if in_offset_context {
return Expr::Lit(n);
}
match lookup(n) {
Some(s) => Expr::Unary("&", Box::new(Expr::StringRef(s))),
None => Expr::Lit(n),
}
}
Expr::Ident(_) | Expr::Raw(_) | Expr::StringRef(_) => expr,
Expr::Deref(inner) => {
let offset_ctx = expr_contains_ident(&inner);
Expr::Deref(Box::new(resolve_strings_inner(*inner, lookup, offset_ctx)))
}
Expr::Unary(op, inner) => Expr::Unary(
op,
Box::new(resolve_strings_inner(*inner, lookup, in_offset_context)),
),
Expr::Binary(op, l, r) => Expr::Binary(
op,
Box::new(resolve_strings_inner(*l, lookup, in_offset_context)),
Box::new(resolve_strings_inner(*r, lookup, in_offset_context)),
),
Expr::Call(name, args) => Expr::Call(
name,
args.into_iter()
.map(|a| resolve_strings_inner(a, lookup, false))
.collect(),
),
Expr::CommaList(items) => Expr::CommaList(
items
.into_iter()
.map(|a| resolve_strings_inner(a, lookup, false))
.collect(),
),
}
}
fn expr_contains_ident(expr: &Expr) -> bool {
match expr {
Expr::Ident(_) => true,
Expr::Lit(_) | Expr::StringRef(_) | Expr::Raw(_) => false,
Expr::Deref(inner) | Expr::Unary(_, inner) => expr_contains_ident(inner),
Expr::Binary(_, l, r) => expr_contains_ident(l) || expr_contains_ident(r),
Expr::Call(_, args) | Expr::CommaList(args) => args.iter().any(expr_contains_ident),
}
}
#[must_use]
pub fn resolve_strings_in_text(text: &str, lookup: &dyn Fn(u64) -> Option<String>) -> String {
let tokens = tokenize(text);
let mut parser = Parser::new(&tokens);
let Some(expr) = parser.parse_expr_or_comma_list() else {
return text.to_string();
};
if !parser.is_done() {
return text.to_string();
}
render(&resolve_strings(expr, lookup))
}
#[must_use]
pub fn simplify_expr(expr: Expr, width: BitWidth) -> Expr {
let mut e = expr;
loop {
let stepped = simplify_step(e.clone(), width);
if stepped == e {
return stepped;
}
e = stepped;
}
}
fn simplify_step(expr: Expr, width: BitWidth) -> Expr {
let mask = width.mask();
match expr {
Expr::Lit(n) => Expr::Lit(n & mask),
Expr::Ident(_) | Expr::Raw(_) | Expr::StringRef(_) => expr,
Expr::Deref(inner) => Expr::Deref(Box::new(simplify_step(*inner, width))),
Expr::Unary(op, inner) => {
let inner = simplify_step(*inner, width);
match (op, &inner) {
("-", Expr::Unary("-", x)) => (**x).clone(),
("~", Expr::Unary("~", x)) => (**x).clone(),
("!", Expr::Unary("!", x)) => (**x).clone(),
("-", Expr::Lit(n)) => Expr::Lit(n.wrapping_neg() & mask),
("~", Expr::Lit(n)) => Expr::Lit((!n) & mask),
_ => Expr::Unary(op, Box::new(inner)),
}
}
Expr::Binary(op, lhs, rhs) => {
let l = simplify_step(*lhs, width);
let r = simplify_step(*rhs, width);
simplify_binary(op, l, r, width)
}
Expr::Call(name, args) => Expr::Call(
name,
args.into_iter().map(|a| simplify_step(a, width)).collect(),
),
Expr::CommaList(items) => {
Expr::CommaList(items.into_iter().map(|a| simplify_step(a, width)).collect())
}
}
}
fn simplify_binary(op: &'static str, l: Expr, r: Expr, width: BitWidth) -> Expr {
let mask = width.mask();
if let (Expr::Lit(a), Expr::Lit(b)) = (&l, &r)
&& let Some(v) = fold_binary(op, *a, *b, width)
{
return Expr::Lit(v & mask);
}
match op {
"+" => {
if let Expr::Lit(0) = l {
return r;
}
if let Expr::Lit(0) = r {
return l;
}
if let (Expr::Binary("+", ll, lr), Expr::Lit(b)) = (&l, &r)
&& let Expr::Lit(a) = **lr
{
let sum = a.wrapping_add(*b) & mask;
if sum == 0 {
return (**ll).clone();
}
return Expr::Binary("+", ll.clone(), Box::new(Expr::Lit(sum)));
}
if let (Expr::Binary("-", ll, lr), Expr::Lit(b)) = (&l, &r)
&& let Expr::Lit(a) = **lr
{
let net = b.wrapping_sub(a);
if net == 0 {
return (**ll).clone();
}
let half = 1u64
<< (match width {
BitWidth::Bits32 => 31,
BitWidth::Bits64 => 63,
});
if (net & mask) >= half {
let abs = net.wrapping_neg() & mask;
return Expr::Binary("-", ll.clone(), Box::new(Expr::Lit(abs)));
}
return Expr::Binary("+", ll.clone(), Box::new(Expr::Lit(net & mask)));
}
}
"-" => {
if let Expr::Lit(0) = r {
return l;
}
if expr_eq(&l, &r) {
return Expr::Lit(0);
}
if let (Expr::Binary("-", ll, lr), Expr::Lit(b)) = (&l, &r)
&& let Expr::Lit(a) = **lr
{
let sum = a.wrapping_add(*b) & mask;
return Expr::Binary("-", ll.clone(), Box::new(Expr::Lit(sum)));
}
if let (Expr::Binary("+", ll, lr), Expr::Lit(b)) = (&l, &r)
&& let Expr::Lit(a) = **lr
{
if a >= *b {
let net = a.wrapping_sub(*b) & mask;
if net == 0 {
return (**ll).clone();
}
return Expr::Binary("+", ll.clone(), Box::new(Expr::Lit(net)));
}
let net = b.wrapping_sub(a) & mask;
return Expr::Binary("-", ll.clone(), Box::new(Expr::Lit(net)));
}
}
"*" => {
if matches!(l, Expr::Lit(0)) || matches!(r, Expr::Lit(0)) {
return Expr::Lit(0);
}
if matches!(l, Expr::Lit(1)) {
return r;
}
if matches!(r, Expr::Lit(1)) {
return l;
}
}
"/" => {
if matches!(r, Expr::Lit(1)) {
return l;
}
if expr_eq(&l, &r) {
return Expr::Lit(1);
}
}
"&" => {
if matches!(l, Expr::Lit(0)) || matches!(r, Expr::Lit(0)) {
return Expr::Lit(0);
}
if let Expr::Lit(n) = r
&& n == mask
{
return l;
}
if let Expr::Lit(n) = l
&& n == mask
{
return r;
}
if expr_eq(&l, &r) {
return l;
}
}
"|" => {
if let Expr::Lit(0) = l {
return r;
}
if let Expr::Lit(0) = r {
return l;
}
if let Expr::Lit(n) = r
&& n == mask
{
return Expr::Lit(mask);
}
if let Expr::Lit(n) = l
&& n == mask
{
return Expr::Lit(mask);
}
if expr_eq(&l, &r) {
return l;
}
}
"^" => {
if let Expr::Lit(0) = l {
return r;
}
if let Expr::Lit(0) = r {
return l;
}
if expr_eq(&l, &r) {
return Expr::Lit(0);
}
}
"<<" | ">>" => {
if matches!(r, Expr::Lit(0)) {
return l;
}
}
_ => {}
}
Expr::Binary(op, Box::new(l), Box::new(r))
}
fn fold_binary(op: &'static str, a: u64, b: u64, width: BitWidth) -> Option<u64> {
let mask = width.mask();
let a = a & mask;
let b = b & mask;
let v = match op {
"+" => a.wrapping_add(b),
"-" => a.wrapping_sub(b),
"*" => a.wrapping_mul(b),
"/" => {
if b == 0 {
return None;
}
a / b
}
"%" => {
if b == 0 {
return None;
}
a % b
}
"&" => a & b,
"|" => a | b,
"^" => a ^ b,
"<<" => a.wrapping_shl((b & 63) as u32),
">>" => a.wrapping_shr((b & 63) as u32),
_ => return None,
};
Some(v & mask)
}
fn expr_eq(a: &Expr, b: &Expr) -> bool {
a == b
}
#[must_use]
pub fn render(expr: &Expr) -> String {
let mut out = String::new();
render_into(&mut out, expr, 0);
out
}
fn render_into(out: &mut String, expr: &Expr, parent_prec: u32) {
let my_prec = expr_prec(expr);
let needs_parens = my_prec < parent_prec;
if needs_parens {
out.push('(');
}
match expr {
Expr::Lit(n) => {
if *n < 10 {
write!(out, "{n}").unwrap();
} else {
write!(out, "{}", format_intel_hex(*n)).unwrap();
}
}
Expr::Ident(name) => out.push_str(name),
Expr::StringRef(s) => {
out.push('"');
for c in s.chars() {
match c {
'"' => out.push_str("\\\""),
'\\' => out.push_str("\\\\"),
'\n' => out.push_str("\\n"),
'\r' => out.push_str("\\r"),
'\t' => out.push_str("\\t"),
c if (c as u32) < 0x20 => {
use std::fmt::Write;
write!(out, "\\x{:02x}", c as u32).unwrap();
}
c => out.push(c),
}
}
out.push('"');
}
Expr::Deref(inner) => {
out.push('[');
render_into(out, inner, 0);
out.push(']');
}
Expr::Unary(op, inner) => {
out.push_str(op);
render_into(out, inner, my_prec);
}
Expr::Binary(op, l, r) => {
render_into(out, l, my_prec);
write!(out, " {op} ").unwrap();
render_into(out, r, my_prec + 1);
}
Expr::Call(name, args) => {
out.push_str(name);
out.push('(');
for (i, a) in args.iter().enumerate() {
if i > 0 {
out.push_str(", ");
}
render_into(out, a, 0);
}
out.push(')');
}
Expr::CommaList(items) => {
for (i, a) in items.iter().enumerate() {
if i > 0 {
out.push_str(", ");
}
render_into(out, a, 0);
}
}
Expr::Raw(s) => out.push_str(s),
}
if needs_parens {
out.push(')');
}
}
fn expr_prec(expr: &Expr) -> u32 {
match expr {
Expr::Lit(_)
| Expr::Ident(_)
| Expr::StringRef(_)
| Expr::Deref(_)
| Expr::Call(_, _)
| Expr::Raw(_) => 100,
Expr::CommaList(_) => 0,
Expr::Unary(_, _) => 90,
Expr::Binary(op, _, _) => match *op {
"*" | "/" | "%" => 80,
"+" | "-" => 70,
"<<" | ">>" => 60,
"&" => 55,
"^" => 50,
"|" => 45,
"<" | "<=" | ">" | ">=" => 40,
"==" | "!=" => 35,
"&&" => 30,
"||" => 25,
_ => 50,
},
}
}
fn format_intel_hex(n: u64) -> String {
let raw = format!("{n:X}");
let needs_zero = raw.chars().next().is_some_and(|c| matches!(c, 'A'..='F'));
if needs_zero {
format!("0{raw}h")
} else {
format!("{raw}h")
}
}
#[cfg(test)]
mod tests {
use super::*;
fn simp(text: &str) -> String {
simplify_text(text, BitWidth::Bits32)
}
#[test]
fn folds_or_with_neg_one() {
assert_eq!(simp("ecx | 0FFFFFFFFh"), "0FFFFFFFFh");
}
#[test]
fn folds_negate_neg_one() {
assert_eq!(simp("~0FFFFFFFFh"), "0");
}
#[test]
fn folds_subtract_self() {
assert_eq!(simp("x - x"), "0");
}
#[test]
fn collapses_sequential_decrement() {
assert_eq!(simp("(x - 1) - 1"), "x - 2");
assert_eq!(simp("((x - 1) - 1) - 1"), "x - 3");
}
#[test]
fn collapses_double_neg() {
assert_eq!(simp("-(-x)"), "x");
assert_eq!(simp("~~x"), "x");
}
#[test]
fn folds_constant_arithmetic() {
assert_eq!(simp("2 + 3"), "5");
assert_eq!(simp("10h - 5"), "0Bh");
}
#[test]
fn preserves_unparseable_input() {
assert_eq!(simp("foo$bar"), "foo$bar");
}
#[test]
fn full_chain_from_runaway() {
let r = simp("~(((((~(ecx | 0FFFFFFFFh)))) & 3) | 0FFFFFFFFh)");
assert!(
!r.contains("ecx | 0FFFFFFFFh"),
"expected the `ecx | 0FFFFFFFFh` subexpr to fold away, got: {r}"
);
}
#[test]
fn comma_list_simplifies_each_arg() {
assert_eq!(simp("x - 0, y | 0, 2 + 3"), "x, y, 5");
}
}