diff --git a/tools/thumb2c/src/main.rs b/tools/thumb2c/src/main.rs index 16d80ea53..6d48aa2bb 100644 --- a/tools/thumb2c/src/main.rs +++ b/tools/thumb2c/src/main.rs @@ -9,7 +9,7 @@ const USAGE: &str = "usage: lifter [args]\n\ score : [--span BYTES] [--name NAME] [--diff ROWS]\n\ lift, compile, and compare against the ROM bytes\n\ tune : [--span BYTES] [--name NAME]\n\ - score, then try call respellings line by line and keep improvements\n\ + score, then try call and literal-pool respellings and keep improvements\n\ batch [--kind WORD] [--limit N] [--all]\n\ score every unregistered retained module (scene and script kinds)\n\ list [--kind WORD] [--all]\n\ diff --git a/tools/thumb2c/src/tune.rs b/tools/thumb2c/src/tune.rs index 4bf627fa5..c108e1905 100644 --- a/tools/thumb2c/src/tune.rs +++ b/tools/thumb2c/src/tune.rs @@ -87,6 +87,10 @@ pub fn tune( lines = stripped; } } + // A symbol address the reference held in HImode files a 64-byte pool + // fix where an SImode spelling files a 1020-byte one, so the reference's + // pool lands earlier. Where the report shows that, each site is respelt. + try_himode(&mut lines, &mut best, &write_and_score, &mut report); let mut index = 0; while index < lines.len() && best.differing > 0 { for alternative in alternatives(&lines[index]) { @@ -136,6 +140,113 @@ mod tests { ); assert!(alternatives(" record[3] = 1;").is_empty()); } + + #[test] + fn respells_a_halfword_store_of_a_symbol_constant() { + assert_eq!( + himode_constant(" *shown = (u16)(s32)Data_00000e00;").as_deref(), + Some(" *shown = 0xe00;") + ); + // A real address is not a halfword, and an ordinary cast is not a + // symbol whose name carries its value. + assert!(himode_constant(" *shown = (u16)(s32)Data_0200e7a0;").is_none()); + assert!(himode_constant(" *shown = (u16)pal_b00;").is_none()); + } + + #[test] + fn respells_a_symbol_local_into_a_halfword_array() { + let unit = |wide: &str| -> Vec { + [ + "extern u8 Data_00000000[];", + "void Lifted_0200423c(void)", + "{", + " s32 p8;", + " u8 *record;", + "", + " p8 = (s32)Data_00000000;", + " record[98] = p8;", + wide, + "}", + ] + .iter() + .map(|line| (*line).to_string()) + .collect() + }; + let respelt = himode_local(&unit(" *(u8 *)(record + 85) = p8;"), "p8").expect("respelt"); + assert_eq!(respelt[3], " u16 p8[1];"); + assert_eq!(respelt[6], " p8[0] = (u16)(s32)Data_00000000;"); + assert_eq!(respelt[7], " record[98] = p8[0];"); + assert_eq!(respelt[8], " *(u8 *)(record + 85) = p8[0];"); + // A word-wide read would lose the bits the truncation drops. + assert!(himode_local(&unit(" *(s32 *)(record + 76) = p8;"), "p8").is_none()); + assert!(himode_local(&unit(" record[85] = p8;"), "record").is_none()); + } + + #[test] + fn respells_a_symbol_local_declared_with_its_initializer() { + let lines: Vec = [ + "extern u8 Data_00000000[];", + "void Lifted_02002aec(void)", + "{", + " u8 *rec7;", + "", + " {", + " s32 data = (s32)Data_00000000;", + "", + " rec7[85] = 3;", + " rec7[34] = data;", + " }", + "}", + ] + .iter() + .map(|line| (*line).to_string()) + .collect(); + let respelt = himode_local(&lines, "data").expect("respelt"); + assert_eq!(respelt[6], " u16 data[1];"); + // The value has no statement of its own, so it opens the block. + assert_eq!(respelt[8], " data[0] = (u16)(s32)Data_00000000;"); + assert_eq!(respelt[9], " rec7[85] = 3;"); + assert_eq!(respelt[10], " rec7[34] = data[0];"); + } + + #[test] + fn takes_a_direct_narrow_store_through_a_halfword_temporary() { + let lines: Vec = [ + "extern u8 Data_00000000[];", + "void Lifted_02002d84(void)", + "{", + " u32 i;", + " u8 *rec7;", + "", + " rec7[34] = (s32)Data_00000000;", + " *(s32 *)(rec7 + 12) = (s32)Data_00000000;", + "}", + ] + .iter() + .map(|line| (*line).to_string()) + .collect(); + let respelt = himode_store(&lines, 6).expect("respelt"); + // The temporary is declared beside the other locals. + assert_eq!(respelt[5], " u16 p0[1];"); + assert_eq!(respelt[7], " p0[0] = (u16)(s32)Data_00000000;"); + assert_eq!(respelt[8], " rec7[34] = p0[0];"); + // A word-wide store keeps the bits the temporary would drop. + assert!(himode_store(&lines, 7).is_none()); + } + + #[test] + fn reads_the_early_pool_signature() { + let report = |row: &str| format!("{}{row}\n", "header\n".repeat(9)); + assert!(pool_is_late(&report( + " ! ldr\tr3, [pc, #288]\t@ (0x678) ldr\tr3, [pc, #60]\t@ (0x594)" + ))); + assert!(!pool_is_late(&report( + " ! ldr\tr3, [pc, #60]\t@ (0x594) ldr\tr3, [pc, #288]\t@ (0x678)" + ))); + assert!(!pool_is_late(&report( + " movs\tr0, #19 movs\tr0, #19" + ))); + } } /// Call statements near each mismatch: for every differing hunk, the callee @@ -284,6 +395,10 @@ pub fn tune_targeted( lines = stripped; } } + // A symbol address the reference held in HImode files a 64-byte pool + // fix where an SImode spelling files a 1020-byte one, so the reference's + // pool lands earlier. Where the report shows that, each site is respelt. + try_himode(&mut lines, &mut best, &write_and_score, &mut report); // Two volatile stores in a row are output-dependent whatever their // offsets (alias analysis never separates volatile accesses), which // ranks the second below an independent instruction after the first @@ -775,3 +890,333 @@ pub fn strip_volatile(lines: &[String], base: &str) -> Vec { }) .collect() } + +/// The pool word a `ldr rX, [pc, #N] @ (0x594)` disassembly column reads. +fn pool_word(column: &str) -> Option { + if !column.contains("[pc, #") { + return None; + } + let at = column.rfind("@ (0x")?; + let hex: String = column[at + 5..] + .chars() + .take_while(|c| c.is_ascii_hexdigit()) + .collect(); + u32::from_str_radix(&hex, 16).ok() +} + +/// Whether the aligned report shows the candidate's literal pool behind the +/// reference's: on some row both columns load a pool word and the +/// candidate's sits at the higher address. GCC 2.96 places a pool by the +/// tightest pending fix, taking each fix's range from the pattern +/// alternative recog chose (`push_minipool_fix`, arm.c:5367) and keeping the +/// entries sorted by `max_address` (`add_minipool_forward_ref`, arm.c:4829), +/// so a late pool is the signature of symbol loads spelled in SImode +/// (`*thumb_movsi_insn` alt 6, range 1020, arm.md:3838) where the reference +/// used a short HImode range (`*thumb_movhi_insn` alt 1, range 64, +/// arm.md:4318). +pub fn pool_is_late(report: &str) -> bool { + report.lines().skip(9).any(|line| { + let Some(at) = line.rfind(" ") else { + return false; + }; + matches!( + (pool_word(&line[..at]), pool_word(&line[at + 2..])), + (Some(candidate), Some(reference)) if candidate > reference + ) + }) +} + +/// A halfword store of a symbol-derived constant respelled as the bare +/// constant: `*p = (u16)(s32)Data_00000e00;` becomes `*p = 0xe00;`. The +/// lifter names such a word by the value the image holds, so the constant is +/// the symbol's own address; through the cast the value is SImode and files a +/// 1020-byte fix, and as a bare halfword it is HImode and files a 64-byte one. +pub fn himode_constant(line: &str) -> Option { + let trimmed = line.trim_start(); + let lead = &line[..line.len() - trimmed.len()]; + let (lhs, value) = trimmed.strip_suffix(';')?.split_once(" = ")?; + let digits = value.strip_prefix("(u16)(s32)Data_")?; + if digits.len() != 8 || !digits.chars().all(|c| c.is_ascii_hexdigit()) { + return None; + } + let constant = u32::from_str_radix(digits, 16).ok()?; + (constant > 0 && constant <= 0xffff).then(|| format!("{lead}{lhs} = {constant:#x};")) +} + +/// The symbol whose address a value takes, in `(s32)Sym` or `(u16)(s32)Sym`. +fn symbol_address(value: &str) -> Option<&str> { + let rest = value + .strip_prefix("(u16)") + .unwrap_or(value) + .strip_prefix("(s32)")?; + let plain = !rest.starts_with(|c: char| c.is_ascii_digit()) + && !rest.is_empty() + && rest.chars().all(|c| c.is_ascii_alphanumeric() || c == '_'); + plain.then_some(rest) +} + +/// Whether a store target is a halfword or narrower, so a value truncated to +/// HImode reaches it unchanged. +fn narrows(lines: &[String], lhs: &str) -> bool { + if [ + "*(u8 *)", + "*(u16 *)", + "*(volatile u8 *)", + "*(volatile u16 *)", + ] + .iter() + .any(|cast| lhs.starts_with(cast)) + { + return true; + } + let Some((base, index)) = lhs.split_once('[') else { + return false; + }; + if !index.ends_with(']') || !base.chars().all(|c| c.is_ascii_alphanumeric() || c == '_') { + return false; + } + lines.iter().any(|line| { + let declaration = line.trim(); + ["u8 *", "u16 *"] + .iter() + .any(|kind| declaration == format!("{kind}{base};")) + || ["u8 ", "u16 "] + .iter() + .any(|kind| declaration.starts_with(&format!("{kind}{base}["))) + }) +} + +/// The scalar locals a rewrite could hold in HImode, in declaration order. +fn himode_names(lines: &[String]) -> Vec { + let mut found: Vec = Vec::new(); + for line in lines { + let declaration = line.trim(); + let Some(name) = declaration + .strip_suffix(';') + .and_then(|d| d.strip_prefix("s32 ").or_else(|| d.strip_prefix("u16 "))) + else { + continue; + }; + let name = name + .split_once(" = ") + .map_or(name, |(declared, _)| declared); + let plain = !name.is_empty() && name.chars().all(|c| c.is_ascii_alphanumeric() || c == '_'); + if plain && !found.iter().any(|held| held == name) { + found.push(name.to_string()); + } + } + found +} + +/// The unit with the scalar local `name` respelled as a one-element `u16` +/// array: `s32 p8; p8 = (s32)Sym;` becomes `u16 p8[1]; p8[0] = +/// (u16)(s32)Sym;` and every read becomes `p8[0]`. A plain `u16` local is +/// promoted back to SImode by `PROMOTE_MODE` (arm.h:618); the array element +/// stays HImode, which is what the reference spelled. None unless the local +/// is declared once, assigned once from a symbol address the unit declares +/// extern, and read only into halfword-or-narrower stores. +pub fn himode_local(lines: &[String], name: &str) -> Option> { + let (mut at_declaration, mut at_assignment) = (None, None); + let mut symbol = ""; + let mut reads = Vec::new(); + for (index, line) in lines.iter().enumerate() { + if !mentions(line, name) { + continue; + } + let statement = line.trim(); + if ["s32 ", "u16 "] + .iter() + .any(|kind| statement == format!("{kind}{name};")) + { + if at_declaration.replace(index).is_some() { + return None; + } + continue; + } + let (lhs, value) = statement.strip_suffix(';')?.split_once(" = ")?; + let declares = ["s32 ", "u16 "] + .iter() + .any(|kind| lhs == format!("{kind}{name}")); + if declares || lhs == name { + if declares && at_declaration.replace(index).is_some() { + return None; + } + if at_assignment.replace(index).is_some() { + return None; + } + symbol = symbol_address(value)?; + continue; + } + if value != name || !narrows(lines, lhs) { + return None; + } + reads.push(index); + } + let (at_declaration, at_assignment) = (at_declaration?, at_assignment?); + if reads.is_empty() || !declares_extern(lines, symbol) { + return None; + } + let lead = |index: usize| { + let line: &str = &lines[index]; + line[..line.len() - line.trim_start().len()].to_string() + }; + let mut out = lines.to_vec(); + out[at_declaration] = format!("{}u16 {name}[1];", lead(at_declaration)); + let assignment = format!("{}{name}[0] = (u16)(s32){symbol};", lead(at_assignment)); + for index in reads { + out[index] = format!("{}[0];", lines[index].trim_end().strip_suffix(';')?); + } + // Written on the declaration itself the value has no statement of its + // own; it becomes the first statement of the block the declaration opens. + if at_assignment == at_declaration { + out.insert(statement_start(lines, at_declaration), assignment); + } else { + out[at_assignment] = assignment; + } + Some(out) +} + +/// A local declaration in the lifter's style, with or without an initializer. +fn is_declaration(line: &str) -> bool { + let statement = line.trim(); + statement.ends_with(';') + && ["s32 ", "s16 ", "u32 ", "u16 ", "u8 "] + .iter() + .any(|kind| statement.starts_with(kind)) +} + +/// Whether the unit declares `symbol` extern, so its address is a link-time +/// constant rather than an ordinary local read. +fn declares_extern(lines: &[String], symbol: &str) -> bool { + lines + .iter() + .any(|line| line.trim_start().starts_with("extern ") && mentions(line, symbol)) +} + +/// The first statement after the declaration at `from`. +fn statement_start(lines: &[String], from: usize) -> usize { + let mut at = from + 1; + while at < lines.len() && (lines[at].trim().is_empty() || is_declaration(&lines[at])) { + at += 1; + } + at +} + +/// The end of the run of locals the enclosing function opens with, so a new +/// local can be declared beside the others. +fn declaration_end(lines: &[String], before: usize) -> Option { + let open = (0..before).rev().find(|&index| lines[index] == "{")?; + let mut at = open + 1; + while at < lines.len() && is_declaration(&lines[at]) { + at += 1; + } + (at > open + 1).then_some(at) +} + +/// A local name the unit does not use. +fn fresh_name(lines: &[String]) -> Option { + (0..100) + .map(|ordinal| format!("p{ordinal}")) + .find(|name| !lines.iter().any(|line| mentions(line, name))) +} + +/// The unit with the direct narrow store at `index`, `rec7[34] = +/// (s32)Sym;`, taken through a one-element `u16` temporary declared beside +/// the other locals. Stored straight the value is SImode and files a +/// 1020-byte pool fix; through the temporary it is HImode and files a +/// 64-byte one. None unless the target is a halfword or narrower and the +/// unit declares the symbol extern. +pub fn himode_store(lines: &[String], index: usize) -> Option> { + let line = &lines[index]; + let statement = line.trim(); + let lead = &line[..line.len() - line.trim_start().len()]; + let (lhs, value) = statement.strip_suffix(';')?.split_once(" = ")?; + let symbol = symbol_address(value)?; + if !narrows(lines, lhs) || !declares_extern(lines, symbol) { + return None; + } + let at = declaration_end(lines, index)?; + let name = fresh_name(lines)?; + let declaration_lead = { + let held: &str = &lines[at - 1]; + &held[..held.len() - held.trim_start().len()] + }; + let mut out = lines.to_vec(); + out[index] = format!("{lead}{lhs} = {name}[0];"); + out.insert(index, format!("{lead}{name}[0] = (u16)(s32){symbol};")); + out.insert(at, format!("{declaration_lead}u16 {name}[1];")); + Some(out) +} + +/// Trials of the two HImode literal-pool respellings, one site at a time, +/// each kept only on a strict improvement and only while the report still +/// shows the candidate's pool behind the reference's. +fn try_himode( + lines: &mut Vec, + best: &mut Score, + score_text: &dyn Fn(&str) -> Result, + report: &mut dyn FnMut(&str), +) { + for index in 0..lines.len() { + if best.differing == 0 || !pool_is_late(&best.report) { + return; + } + let Some(alternative) = himode_constant(&lines[index]) else { + continue; + }; + let saved = std::mem::replace(&mut lines[index], alternative); + match score_text(&format!("{}\n", lines.join("\n"))) { + Ok(trial) if trial.differing < best.differing => { + report(&format!( + "{} -> {} himode {}", + best.differing, + trial.differing, + lines[index].trim() + )); + *best = trial; + } + _ => lines[index] = saved, + } + } + for name in himode_names(lines) { + if best.differing == 0 || !pool_is_late(&best.report) { + return; + } + let Some(rewritten) = himode_local(lines, &name) else { + continue; + }; + match score_text(&format!("{}\n", rewritten.join("\n"))) { + Ok(trial) if trial.differing < best.differing => { + report(&format!( + "{} -> {} himode {name}[1]", + best.differing, trial.differing + )); + *best = trial; + *lines = rewritten; + } + _ => {} + } + } + let mut index = 0; + while index < lines.len() { + if best.differing == 0 || !pool_is_late(&best.report) { + return; + } + if let Some(rewritten) = himode_store(lines, index) { + if let Ok(trial) = score_text(&format!("{}\n", rewritten.join("\n"))) { + if trial.differing < best.differing { + report(&format!( + "{} -> {} himode store {}", + best.differing, + trial.differing, + lines[index].trim() + )); + *best = trial; + *lines = rewritten; + index += 2; + } + } + } + index += 1; + } +}