Everybody loves a good yearly or so update

This commit is contained in:
KholdFuzion
2025-04-25 10:11:31 -04:00
parent 02bad2cca4
commit 414a8feb8e
373 changed files with 46731 additions and 50709 deletions
+518
View File
@@ -0,0 +1,518 @@
mod postprocess;
mod preprocess;
use std::{
borrow::Cow,
ffi::OsString,
fmt::Display,
fs::{self, File},
io::Write,
path::{Path, PathBuf},
process::{exit, Command},
str::FromStr,
};
use anyhow::Result;
use argp::{EarlyExit, FromArgs, HelpStyle};
use encoding_rs::EUC_JP;
use enum_map::{Enum, EnumMap};
use temp_dir::TempDir;
use postprocess::fixup_objfile;
use preprocess::parse_source;
#[derive(Copy, Clone, Eq, PartialEq, Debug, Enum)]
enum OutputSection {
Text,
Data,
Rodata,
Bss,
}
impl OutputSection {
fn as_str(&self) -> &'static str {
match self {
OutputSection::Text => ".text",
OutputSection::Data => ".data",
OutputSection::Rodata => ".rodata",
OutputSection::Bss => ".bss",
}
}
}
impl Display for OutputSection {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.as_str())
}
}
#[derive(Clone, Debug)]
struct Function {
text_glabels: Vec<String>,
asm_conts: Vec<String>,
late_rodata_dummy_bytes: Vec<[u8; 4]>,
jtbl_rodata_size: usize,
late_rodata_asm_conts: Vec<String>,
fn_desc: String,
data: EnumMap<OutputSection, (Option<String>, usize)>,
}
#[derive(Clone, Copy, Debug)]
enum Encoding {
Latin1,
Custom(&'static encoding_rs::Encoding),
}
impl Encoding {
fn encode<'a>(&self, s: &'a str) -> Result<Cow<'a, [u8]>> {
match self {
Encoding::Latin1 => {
if encoding_rs::mem::is_str_latin1(s) {
return Ok(encoding_rs::mem::encode_latin1_lossy(s));
}
}
Encoding::Custom(enc) => {
if *enc == EUC_JP {
let s = s.replace("", "");
let (ret, _, failed) = enc.encode(&s);
if !failed {
return Ok(Cow::Owned(ret.into_owned()));
}
} else {
let (ret, _, failed) = enc.encode(s);
if !failed {
return Ok(ret);
}
}
}
}
Err(anyhow::anyhow!("Failed to encode string: {}", s))
}
fn decode<'a>(&self, bytes: &'a [u8]) -> Result<Cow<'a, str>> {
match self {
Encoding::Latin1 => Ok(encoding_rs::mem::decode_latin1(bytes)),
Encoding::Custom(enc) => {
let (ret, _, failed) = enc.decode(bytes);
if !failed {
Ok(ret)
} else {
Err(anyhow::anyhow!("Failed to decode string: {}", ret))
}
}
}
}
}
impl FromStr for Encoding {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
if s == "latin1" {
Ok(Encoding::Latin1)
} else {
match encoding_rs::Encoding::for_label(s.as_bytes()) {
Some(enc) => Ok(Encoding::Custom(enc)),
None => Err(format!("Unsupported encoding: {}", s)),
}
}
}
}
#[derive(Copy, Clone, Debug, PartialEq)]
enum ConvertStatics {
No,
Local,
Global,
GlobalWithFilename,
}
impl FromStr for ConvertStatics {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Ok(match s {
"no" => ConvertStatics::No,
"local" => ConvertStatics::Local,
"global" => ConvertStatics::Global,
"global-with-filename" => ConvertStatics::GlobalWithFilename,
_ => return Err("invalid value for symbol visibility".into()),
})
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
enum OptLevel {
O0,
O1,
O2,
G,
G3,
}
/// Pre-process .c files and post-process .o files to enable embedding MIPS assembly into IDO-compiled C.
#[derive(FromArgs)]
struct AsmProcArgs {
/// Path to a file containing a prelude to the assembly file (with .set and .macro directives, e.g.)
#[argp(option, arg_name = "FILE")]
asm_prelude: Option<PathBuf>,
/// Input encoding (default: latin1)
#[argp(
option,
default = "Encoding::Latin1",
from_str_fn(FromStr::from_str),
arg_name = "ENCODING"
)]
input_enc: Encoding,
/// Output encoding (default: latin1)
#[argp(
option,
default = "Encoding::Latin1",
from_str_fn(FromStr::from_str),
arg_name = "ENCODING"
)]
output_enc: Encoding,
/// Drop mdebug and gptab sections
#[argp(switch)]
drop_mdebug_gptab: bool,
/// Change symbol visibility for static variables. Mode must be one of:
/// no, local, global, global-with-filename (default: local)
#[argp(
option,
default = "ConvertStatics::Local",
from_str_fn(FromStr::from_str),
arg_name = "MODE"
)]
convert_statics: ConvertStatics,
/// Force processing of files without GLOBAL_ASM blocks
#[argp(switch)]
force: bool,
/// Emit temporary files to this directory
#[argp(option, arg_name = "DIR")]
keep_preprocessed: Option<PathBuf>,
/// Replace floats with their encoded hexadecimal representation in CutsceneData data
#[argp(switch)]
encode_cutscene_data_floats: bool,
/// Don't generate a .d make dependency file
#[argp(switch)]
no_dep_file: bool,
#[argp(positional, greedy)]
rest: Vec<String>,
}
struct CompileOpts {
opt: OptLevel,
framepointer: bool,
mips1: bool,
kpic: bool,
pascal: bool,
}
fn extract_compiler_input_output(
compile_args: &[String],
) -> Result<(PathBuf, PathBuf, Vec<String>), &'static str> {
let mut compile_args: Vec<String> = compile_args.to_vec();
let out_ind = compile_args
.iter()
.position(|arg| arg == "-o")
.ok_or("missing -o argument")?;
let out_filename = compile_args
.get(out_ind + 1)
.ok_or("missing argument after -o")?
.clone();
compile_args.remove(out_ind + 1);
compile_args.remove(out_ind);
let in_file_str = compile_args
.last()
.ok_or("missing input file argument")?
.clone();
compile_args.pop();
let out_file: PathBuf = out_filename.into();
let in_file: PathBuf = in_file_str.into();
Ok((in_file, out_file, compile_args))
}
fn parse_compile_args(
compile_args: &[String],
in_file: &Path,
) -> Result<CompileOpts, &'static str> {
let mut opt_flags = vec![];
for x in compile_args {
opt_flags.push(match x.as_str() {
"-g" => OptLevel::G,
"-O0" => OptLevel::O0,
"-O1" => OptLevel::O1,
"-O2" => OptLevel::O2,
_ => continue,
});
}
if opt_flags.len() != 1 {
return Err("exactly one of -g/-O0/-O1/-O2 must be passed");
}
let mut opt = opt_flags[0];
let mips1 = !compile_args.contains(&"-mips2".to_string());
let framepointer = compile_args.contains(&"-framepointer".to_string());
let kpic = compile_args.contains(&"-KPIC".to_string());
if compile_args.contains(&"-g3".to_string()) {
if opt != OptLevel::O2 {
return Err("-g3 is only supported together with -O2");
}
opt = OptLevel::G3;
}
if mips1 && (!matches!(opt, OptLevel::O1 | OptLevel::O2) || framepointer) {
return Err("-mips1 is only supported together with -O1 or -O2");
}
let in_file_str = in_file.to_string_lossy();
let pascal = in_file_str.ends_with(".p")
|| in_file_str.ends_with(".pas")
|| in_file_str.ends_with(".pp");
if pascal && !matches!(opt, OptLevel::O1 | OptLevel::O2 | OptLevel::G3) {
return Err("Pascal is only supported together with -O1, -O2 or -O2 -g3");
}
Ok(CompileOpts {
opt,
framepointer,
mips1,
kpic,
pascal,
})
}
fn parse_rest(rest: &[String]) -> Option<(&[String], &[String], &[String])> {
let mut iter = rest.splitn(3, |x| *x == "--");
let compiler = iter.next()?;
let assembler = iter.next()?;
let compile_args = iter.next()?;
assert!(iter.next().is_none());
Some((compiler, assembler, compile_args))
}
struct ParsedArgs {
args: AsmProcArgs,
compiler: Vec<String>,
assembler: Vec<String>,
compile_args: Vec<String>,
in_file: PathBuf,
out_file: PathBuf,
opts: CompileOpts,
}
/// Parse command line arguments while allowing --a=b syntax.
///
/// This provides backward compatibility with Python argparse.
fn from_args_allow_eq(progname: &[&str], args: &[OsString]) -> Result<AsmProcArgs, EarlyExit> {
let base_res = AsmProcArgs::from_args(progname, args);
if base_res.is_ok() {
return base_res;
}
// Try splitting up = chars successively, until we get a valid parse.
// This ensures we don't impact the compiler/assembler parts.
//
// Technically this might end up splitting --a --b=c into --a --b c even
// where --a is a flag that takes an argument --b=c, but this seems unlikely
// with our use case.
let mut i = 0;
let mut args = args.to_vec();
while i < args.len() {
let arg = args[i].as_encoded_bytes();
if arg.starts_with(b"--") {
if let Some(eq) = arg.iter().position(|&x| x == b'=') {
// SAFETY: splitting on ASCII still results in valid encoded bytes
let before = unsafe { OsString::from_encoded_bytes_unchecked(arg[0..eq].into()) };
let after = unsafe { OsString::from_encoded_bytes_unchecked(arg[eq + 1..].into()) };
args.splice(i..i + 1, [before, after]);
let new_res = AsmProcArgs::from_args(progname, &args);
if new_res.is_ok() {
return new_res;
}
i += 1;
}
}
i += 1;
}
base_res
}
fn parse_args_or_exit() -> ParsedArgs {
let argv: Vec<_> = std::env::args_os().collect();
let help_style = HelpStyle {
short_usage: true,
..HelpStyle::default()
};
let progname = argv[0].to_string_lossy();
let args = from_args_allow_eq(&[&progname], &argv[1..]).unwrap_or_else(|early_exit| {
exit(match early_exit {
EarlyExit::Help(help) => {
println!(
"{}",
help.generate(&help_style).replace(
"[rest...]",
"<compiler...> -- <assembler...> -- <compiler flags...>"
)
);
0
}
EarlyExit::Err(err) => {
eprintln!("{}\nRun {} --help for more information.", err, progname);
1
}
})
});
let Some((compiler, assembler, compile_args)) = parse_rest(&args.rest) else {
eprintln!(
"Usage: {} [options] <compiler...> -- <assembler...> -- <compiler flags...>",
progname
);
eprintln!("Run {} --help for more information.", progname);
exit(1);
};
let (in_file, out_file, compile_args) = extract_compiler_input_output(compile_args)
.unwrap_or_else(|err| {
eprintln!("Failed to parse compiler flags: {}", err);
exit(1);
});
let opts = parse_compile_args(&compile_args, &in_file).unwrap_or_else(|err| {
eprintln!("Unsupported compiler flags: {}", err);
exit(1);
});
let compiler = compiler.into();
let assembler = assembler.into();
ParsedArgs {
args,
compiler,
assembler,
compile_args,
in_file,
out_file,
opts,
}
}
fn main() -> Result<()> {
let ParsedArgs {
args,
compiler,
assembler,
compile_args,
in_file,
out_file,
opts,
} = parse_args_or_exit();
let assembler_sh = assembler
.iter()
.map(|s| shlex::try_quote(s).unwrap().into_owned())
.collect::<Vec<String>>()
.join(" ");
let in_dir = fs::canonicalize(in_file.parent().unwrap().join("."))?;
let temp_dir = TempDir::with_prefix("asm_processor")?;
let preprocessed_filename = format!(
"preprocessed_{}",
in_file.file_name().unwrap().to_str().unwrap()
);
let preprocessed_path = temp_dir.path().join(&preprocessed_filename);
let mut preprocessed_file = File::create(&preprocessed_path)?;
let res = parse_source(&in_file, &args, &opts, true)?;
preprocessed_file.write_all(&res.output)?;
if let Some(keep_output_dir) = &args.keep_preprocessed {
fs::create_dir_all(keep_output_dir)?;
fs::copy(
&preprocessed_path,
keep_output_dir.join(&preprocessed_filename),
)?;
}
// Run compiler
let mut compile_command = Command::new(&compiler[0]);
compile_command
.args(&compile_args)
.arg("-I")
.arg(in_dir)
.arg("-o")
.arg(&out_file)
.arg(&preprocessed_path);
match compile_command.status() {
Ok(status) if status.success() => {}
_ => {
return Err(anyhow::anyhow!(
"Failed to compile file {}. Command line:\n\n{:?}\n",
in_file.display(),
compile_command
));
}
}
if !res.functions.is_empty() || args.force {
let prelude_str;
let asm_prelude = match &args.asm_prelude {
Some(prelude) => {
if let Ok(res) = fs::read_to_string(prelude) {
prelude_str = res;
&prelude_str
} else {
return Err(anyhow::anyhow!("Failed to read asm prelude"));
}
}
None => include_str!("../../prelude.inc"),
};
fixup_objfile(
&out_file,
&res.functions,
asm_prelude,
&assembler_sh,
&args.output_enc,
args.drop_mdebug_gptab,
args.convert_statics,
)?;
}
if !res.deps.is_empty() && !args.no_dep_file {
let deps_file = out_file.with_extension("asmproc.d");
let mut deps_file = File::create(&deps_file)?;
writeln!(
deps_file,
"{}: {}",
out_file.to_str().unwrap(),
res.deps.join(" \\\n ")
)?;
for dep in res.deps {
writeln!(deps_file, "\n{dep}:")?;
}
}
Ok(())
}
File diff suppressed because it is too large Load Diff
+961
View File
@@ -0,0 +1,961 @@
use std::{fs, io::Write, iter, path::Path, sync::OnceLock};
use anyhow::Result;
use enum_map::{Enum, EnumMap};
use regex_lite::Regex;
use crate::{AsmProcArgs, CompileOpts, Encoding, Function, OptLevel, OutputSection};
#[derive(Copy, Clone, Eq, PartialEq, Debug, Enum)]
enum InputSection {
Text,
Data,
Rodata,
LateRodata,
Bss,
}
impl InputSection {
fn from_str(name: &str) -> Option<InputSection> {
match name {
".text" => Some(InputSection::Text),
".data" => Some(InputSection::Data),
".rodata" => Some(InputSection::Rodata),
".late_rodata" => Some(InputSection::LateRodata),
".bss" => Some(InputSection::Bss),
_ => None,
}
}
}
#[derive(Clone, Debug)]
struct GlobalState {
late_rodata_hex: u32,
valuectr: usize,
namectr: usize,
min_instr_count: usize,
skip_instr_count: usize,
use_jtbl_for_rodata: bool,
prelude_if_late_rodata: usize,
mips1: bool,
pascal: bool,
}
impl GlobalState {
fn new(
min_instr_count: usize,
skip_instr_count: usize,
use_jtbl_for_rodata: bool,
prelude_if_late_rodata: usize,
mips1: bool,
pascal: bool,
) -> Self {
Self {
// A value that hopefully never appears as a 32-bit rodata constant (or we
// miscompile late rodata). Increases by 1 in each step.
late_rodata_hex: 0xE0123456,
valuectr: 0,
namectr: 0,
min_instr_count,
skip_instr_count,
use_jtbl_for_rodata,
prelude_if_late_rodata,
mips1,
pascal,
}
}
fn next_late_rodata_hex(&mut self) -> [u8; 4] {
let dummy_bytes = self.late_rodata_hex.to_be_bytes();
if (self.late_rodata_hex & 0xffff) == 0 {
// Avoid lui
self.late_rodata_hex += 1;
}
self.late_rodata_hex += 1;
dummy_bytes
}
fn make_name(&mut self, cat: &str) -> String {
self.namectr += 1;
format!("_asmpp_{}{}", cat, self.namectr)
}
fn func_prologue(&self, name: &str) -> String {
if self.pascal {
[
&format!("procedure {}();", name),
"type",
" pi = ^integer;",
" pf = ^single;",
" pd = ^double;",
"var",
" vi: pi;",
" vf: pf;",
" vd: pd;",
"begin",
" vi := vi;",
" vf := vf;",
" vd := vd;",
]
.join(" ")
} else {
format!("void {}(void) {{", name)
}
}
fn func_epilogue(&self) -> String {
if self.pascal {
"end;".to_string()
} else {
'}'.to_string()
}
}
fn pascal_assignment_float(&mut self, val: f32) -> String {
self.valuectr += 1;
let address = (8 * self.valuectr) & 0x7FFF;
format!("vf := pf({}); vf^ := {:?};", address, val)
}
fn pascal_assignment_double(&mut self, val: f64) -> String {
self.valuectr += 1;
let address = (8 * self.valuectr) & 0x7FFF;
format!("vd := pd({}); vd^ := {:?};", address, val)
}
fn pascal_assignment_int(&mut self, val: i32) -> String {
self.valuectr += 1;
let address = (8 * self.valuectr) & 0x7FFF;
format!("vi := pi({}); vi^ := {};", address, val)
}
}
#[derive(Clone, Debug)]
struct GlobalAsmBlock {
fn_desc: String,
cur_section: InputSection,
asm_conts: Vec<String>,
late_rodata_asm_conts: Vec<String>,
late_rodata_alignment: usize,
late_rodata_alignment_from_context: bool,
text_glabels: Vec<String>,
fn_section_sizes: EnumMap<InputSection, usize>,
fn_ins_inds: Vec<(usize, usize)>,
glued_line: String,
num_lines: usize,
}
impl GlobalAsmBlock {
fn new(fn_desc: String) -> Self {
Self {
fn_desc,
cur_section: InputSection::Text,
asm_conts: vec![],
late_rodata_asm_conts: vec![],
late_rodata_alignment: 0,
late_rodata_alignment_from_context: false,
text_glabels: vec![],
fn_section_sizes: EnumMap::default(),
fn_ins_inds: vec![],
glued_line: String::new(),
num_lines: 0,
}
}
fn fail_without_line<T>(&self, msg: &str) -> Result<T> {
Err(anyhow::anyhow!("{}\nwithin {}", msg, self.fn_desc))
}
fn fail_at_line<T>(&self, msg: &str, line: &str) -> Result<T> {
Err(anyhow::anyhow!(
"{}\nwithin {} at line {}",
msg,
self.fn_desc,
line
))
}
fn count_quoted_size(
&self,
line: &str,
z: bool,
real_line: &str,
output_enc: &Encoding,
) -> Result<usize> {
let line = output_enc.encode(line)?;
let mut in_quote = false;
let mut has_comma = true;
let mut num_parts = 0;
let mut ret = 0;
let mut i = 0;
let digits = b"0123456789"; // 0-7 would be more sane, but this matches GNU as
let hexdigits = b"0123456789abcdefABCDEF";
while i < line.len() {
let c = line[i];
i += 1;
if !in_quote {
if c == b'"' {
in_quote = true;
if z && !has_comma {
return self.fail_at_line(".asciiz with glued strings is not supported due to GNU as version diffs", real_line);
}
num_parts += 1;
} else if c == b',' {
has_comma = true;
}
} else {
if c == b'"' {
in_quote = false;
has_comma = false;
continue;
}
ret += 1;
if c != b'\\' {
continue;
}
if i == line.len() {
return self.fail_at_line("backslash at end of line not supported", real_line);
}
let c = line[i];
i += 1;
// (if c is in "bfnrtv", we have a real escaped literal)
if c == b'x' {
// hex literal, consume any number of hex chars, possibly none
while i < line.len() && hexdigits.contains(&line[i]) {
i += 1;
}
} else if digits.contains(&c) {
// octal literal, consume up to two more digits
let mut it = 0;
while i < line.len() && digits.contains(&line[i]) && it < 2 {
i += 1;
it += 1;
}
}
}
}
if in_quote {
return self.fail_at_line("unterminated string literal", real_line);
}
if num_parts == 0 {
return self.fail_at_line(".ascii with no string", real_line);
}
Ok(ret + if z { num_parts } else { 0 })
}
fn align(&mut self, n: usize) {
let size = &mut self.fn_section_sizes[self.cur_section];
while *size % n != 0 {
*size += 1;
}
}
fn add_sized(&mut self, size: isize, line: &str) -> Result<()> {
if (self.cur_section == InputSection::Text || self.cur_section == InputSection::LateRodata)
&& size % 4 != 0
{
return self.fail_at_line("size must be a multiple of 4", line);
}
if size < 0 {
return self.fail_at_line("size cannot be negative", line);
}
self.fn_section_sizes[self.cur_section] += size as usize;
if self.cur_section == InputSection::Text {
if self.text_glabels.is_empty() {
return self.fail_at_line(".text block without an initial glabel", line);
}
self.fn_ins_inds
.push((self.num_lines - 1, size as usize / 4));
}
Ok(())
}
fn process_line(&mut self, line: &str, output_enc: &Encoding) -> Result<()> {
self.num_lines += 1;
if let Some(stripped) = line.strip_suffix("\\") {
self.glued_line = format!("{}{}", self.glued_line, stripped);
return Ok(());
}
let mut line = self.glued_line.clone() + line;
self.glued_line = String::new();
static CACHE: OnceLock<(Regex, Regex)> = OnceLock::new();
let (re_comment_or_string, re_label) = CACHE.get_or_init(|| {
(
Regex::new(r#"#.*|/\*.*?\*/|"(?:\\.|[^\\"])*""#).unwrap(),
Regex::new(r"^[a-zA-Z0-9_]+:\s*").unwrap(),
)
});
fn re_comment_replacer(caps: &regex_lite::Captures) -> String {
let s = caps[0].to_string();
if s.starts_with("/") || s.starts_with("#") {
" ".to_owned()
} else {
s
}
}
let real_line = line.clone();
line = re_comment_or_string
.replace_all(&line, re_comment_replacer)
.into_owned();
line = line.trim().to_string();
line = re_label.replace_all(&line, "").into_owned();
let mut changed_section = false;
let mut emitting_double = false;
if (line.starts_with("glabel ") || line.starts_with("jlabel "))
&& self.cur_section == InputSection::Text
{
self.text_glabels
.push(line.split_whitespace().nth(1).unwrap().to_string());
}
if line.is_empty() {
// empty line
} else if line.starts_with("glabel ")
|| line.starts_with("dlabel ")
|| line.starts_with("jlabel ")
|| line.starts_with("endlabel ")
|| (!line.contains(" ") && line.ends_with(":"))
{
// label
} else if line.starts_with(".section")
|| matches!(
line.as_str(),
".text" | ".data" | ".rdata" | ".rodata" | ".bss" | ".late_rodata"
)
{
// section change
self.cur_section = if line == ".rdata" {
InputSection::Rodata
} else {
let first_arg = line.split(',').next().unwrap().to_string();
let name = first_arg.split_whitespace().last().unwrap();
match InputSection::from_str(name) {
Some(s) => s,
None => {
return self.fail_at_line("unrecognized .section directive", &real_line)
}
}
};
changed_section = true;
} else if line.starts_with(".late_rodata_alignment") {
if self.cur_section != InputSection::LateRodata {
return self.fail_at_line(
".late_rodata_alignment must occur within .late_rodata section",
&real_line,
);
}
let value = line.split_whitespace().nth(1).unwrap().parse::<usize>()?;
if value != 4 && value != 8 {
return self
.fail_at_line(".late_rodata_alignment argument must be 4 or 8", &real_line);
}
if self.late_rodata_alignment != 0 && self.late_rodata_alignment != value {
return self.fail_without_line(
".late_rodata_alignment alignment assumption conflicts with earlier .double directive. Make sure to provide explicit alignment padding."
);
}
self.late_rodata_alignment = value;
changed_section = true;
} else if line.starts_with(".incbin") {
let size = line.split(',').last().unwrap().trim().parse::<isize>()?;
self.add_sized(size, &real_line)?;
} else if line.starts_with(".word")
|| line.starts_with(".gpword")
|| line.starts_with(".float")
{
self.align(4);
self.add_sized(4 * line.split(',').count() as isize, &real_line)?;
} else if line.starts_with(".double") {
self.align(4);
if self.cur_section == InputSection::LateRodata {
let align8 = self.fn_section_sizes[self.cur_section] % 8;
// Automatically set late_rodata_alignment, so the generated C code uses doubles.
// This gives us correct alignment for the transferred doubles even when the
// late_rodata_alignment is wrong, e.g. for non-matching compilation.
if self.late_rodata_alignment == 0 {
self.late_rodata_alignment = 8 - align8;
self.late_rodata_alignment_from_context = true;
} else if self.late_rodata_alignment != 8 - align8 {
if self.late_rodata_alignment_from_context {
return self.fail_at_line(
"found two .double directives with different start addresses mod 8. Make sure to provide explicit alignment padding.",
&real_line
);
} else {
return self.fail_at_line(
".double at address that is not 0 mod 8 (based on .late_rodata_alignment assumption). Make sure to provide explicit alignment padding.\n{}",
&real_line
);
}
}
self.add_sized(8 * line.split(',').count() as isize, &real_line)?;
emitting_double = true;
}
} else if line.starts_with(".space") {
let size = line.split_whitespace().nth(1).unwrap().parse::<isize>()?;
self.add_sized(size, &real_line)?;
} else if line.starts_with(".balign") {
let align = line.split_whitespace().nth(1).unwrap().parse::<isize>()?;
if align != 4 {
return self.fail_at_line("only .balign 4 is supported", &real_line);
}
self.align(4);
} else if line.starts_with(".align") {
let align = line.split_whitespace().nth(1).unwrap().parse::<isize>()?;
if align != 2 {
return self.fail_at_line("only .align 2 is supported", &real_line);
}
self.align(4);
} else if line.starts_with(".asci") {
let z = line.starts_with(".asciz") || line.starts_with(".asciiz");
self.add_sized(
self.count_quoted_size(&line, z, &real_line, output_enc)? as isize,
&real_line,
)?;
} else if line.starts_with(".byte") {
self.add_sized(line.split(',').count() as isize, &real_line)?;
} else if line.starts_with(".half")
|| line.starts_with(".hword")
|| line.starts_with(".short")
{
self.align(2);
self.add_sized(2 * line.split(',').count() as isize, &real_line)?;
} else if line.starts_with(".size") {
} else if line.starts_with('.') {
return self.fail_at_line("asm directive not supported", &real_line);
} else {
// Unfortunately, macros are hard to support for .rodata --
// we don't know how how space they will expand to before
// running the assembler, but we need that information to
// construct the C code. So if we need that we'll either
// need to run the assembler twice (at least in some rare
// cases), or change how this program is invoked.
// Similarly, we can't currently deal with pseudo-instructions
// that expand to several real instructions.
if self.cur_section != InputSection::Text {
return self.fail_at_line(
"instruction or macro call in non-.text section? not supported",
&real_line,
);
}
self.add_sized(4, &real_line)?;
}
if self.cur_section == InputSection::LateRodata {
if !changed_section {
if emitting_double {
self.late_rodata_asm_conts.push(".align 0".to_string());
}
self.late_rodata_asm_conts.push(real_line.clone());
if emitting_double {
self.late_rodata_asm_conts.push(".align 2".to_string());
}
}
} else {
self.asm_conts.push(real_line.clone());
}
Ok(())
}
const MAX_FN_SIZE: usize = 100;
fn finish(&self, state: &mut GlobalState) -> Result<(Vec<String>, Function)> {
let mut src = vec!["".to_owned(); self.num_lines + 1];
let mut late_rodata_dummy_bytes = vec![];
let mut jtbl_rodata_size = 0;
let mut late_rodata_fn_output = vec![];
let num_instr = self.fn_section_sizes[InputSection::Text] / 4;
if self.fn_section_sizes[InputSection::LateRodata] > 0 {
// Generate late rodata by emitting unique float constants.
// This requires 3 instructions for each 4 bytes of rodata.
// If we know alignment, we can use doubles, which give 3
// instructions for 8 bytes of rodata.
let size = self.fn_section_sizes[InputSection::LateRodata] / 4;
let mut skip_next = false;
let mut needs_double = self.late_rodata_alignment != 0;
let mut extra_mips1_nop = false;
let (jtbl_size, jtbl_min_rodata_size) = match (state.pascal, state.mips1) {
(true, true) => (9, 2),
(true, false) => (8, 2),
(false, true) => (11, 5),
(false, false) => (9, 5),
};
for i in 0..size {
if skip_next {
skip_next = false;
continue;
}
// Jump tables give 9 instructions (11 with -mips1) for >= 5 words of rodata,
// and should be emitted when:
// - -O2 or -O2 -g3 are used, which give the right codegen
// - we have emitted our first .float/.double (to ensure that we find the
// created rodata in the binary)
// - we have emitted our first .double, if any (to ensure alignment of doubles
// in shifted rodata sections)
// - we have at least 5 words of rodata left to emit (otherwise IDO does not
// generate a jump table)
// - we have at least 10 more instructions to go in this function (otherwise our
// function size computation will be wrong since the delay slot goes unused)
if !needs_double
&& state.use_jtbl_for_rodata
&& i >= 1
&& size - i >= jtbl_min_rodata_size
&& num_instr - late_rodata_fn_output.len() >= jtbl_size + 1
{
let line = if state.pascal {
let cases: String = (0..(size - i))
.map(|case| format!("{}: ;", case))
.collect::<Vec<String>>()
.join("\n");
format!("case 0 of {} otherwise end;", cases)
} else {
let cases: String = (0..(size - i))
.map(|case| format!("case {}:", case))
.collect::<Vec<String>>()
.join(" ");
format!("switch (*(volatile int*)0) {{ {} ; }}", cases)
};
late_rodata_fn_output.push(line);
late_rodata_fn_output.extend(iter::repeat("".to_owned()).take(jtbl_size - 1));
jtbl_rodata_size = (size - i) * 4;
extra_mips1_nop = i != 2;
break;
}
let dummy_bytes = state.next_late_rodata_hex();
late_rodata_dummy_bytes.push(dummy_bytes);
if self.late_rodata_alignment == 4 * ((i + 1) % 2 + 1) && i + 1 < size {
let dummy_bytes2 = state.next_late_rodata_hex();
late_rodata_dummy_bytes.push(dummy_bytes2);
let combined = [dummy_bytes, dummy_bytes2].concat().try_into().unwrap();
let fval = f64::from_be_bytes(combined);
let line = if state.pascal {
state.pascal_assignment_double(fval)
} else {
format!("*(volatile double*)0 = {:?};", fval)
};
late_rodata_fn_output.push(line);
skip_next = true;
needs_double = false;
if state.mips1 {
// mips1 does not have ldc1/sdc1
late_rodata_fn_output.push("".to_owned());
late_rodata_fn_output.push("".to_owned());
}
extra_mips1_nop = false;
} else {
let fval = f32::from_be_bytes(dummy_bytes);
let line = if state.pascal {
state.pascal_assignment_float(fval)
} else {
format!("*(volatile float*)0 = {:?}f;", fval)
};
late_rodata_fn_output.push(line);
extra_mips1_nop = true;
}
late_rodata_fn_output.push("".to_owned());
late_rodata_fn_output.push("".to_owned());
}
if state.mips1 && extra_mips1_nop {
late_rodata_fn_output.push("".to_owned());
}
}
let mut text_name = None;
if self.fn_section_sizes[InputSection::Text] > 0 || !late_rodata_fn_output.is_empty() {
let new_name = state.make_name("func");
src[0] = state.func_prologue(&new_name);
text_name = Some(new_name);
src[self.num_lines] = state.func_epilogue();
let instr_count = self.fn_section_sizes[InputSection::Text] / 4;
if instr_count < state.min_instr_count {
return self.fail_without_line("too short .text block");
}
let mut tot_emitted = 0;
let mut tot_skipped = 0;
let mut fn_emitted = 0;
let mut fn_skipped = 0;
let mut skipping = true;
let mut rodata_stack: Vec<String> = late_rodata_fn_output.clone();
rodata_stack.reverse();
for &(line, count) in &self.fn_ins_inds {
for _ in 0..count {
if fn_emitted > Self::MAX_FN_SIZE
&& instr_count - tot_emitted > state.min_instr_count
&& (rodata_stack.is_empty() || !rodata_stack.last().unwrap().is_empty())
{
// Don't let functions become too large. When a function reaches 284
// instructions, and -O2 -framepointer flags are passed, the IRIX
// compiler decides it is a great idea to start optimizing more.
// Also, Pascal cannot handle too large functions before it runs out
// of unique statements to write.
fn_emitted = 0;
fn_skipped = 0;
skipping = true;
let large_func_name = state.make_name("large_func");
src[line] += &format!(
" {} {} ",
state.func_epilogue(),
state.func_prologue(&large_func_name)
);
}
let skip_for_late_rodata = if !rodata_stack.is_empty() {
state.prelude_if_late_rodata
} else {
0
};
if skipping && fn_skipped < state.skip_instr_count + skip_for_late_rodata {
fn_skipped += 1;
tot_skipped += 1;
} else {
skipping = false;
if let Some(entry) = rodata_stack.pop() {
src[line] += &entry;
} else if state.pascal {
src[line] += &state.pascal_assignment_int(0);
} else {
src[line] += "*(volatile int*)0 = 0;";
}
}
tot_emitted += 1;
fn_emitted += 1;
}
}
if !rodata_stack.is_empty() {
let size = late_rodata_fn_output.len() / 3;
let available = instr_count - tot_skipped;
return self.fail_without_line(&format!(
"late rodata to text ratio is too high: {} / {} must be <= 1/3\n
add .late_rodata_alignment (4|8) to the .late_rodata block
to double the allowed ratio.",
size, available
));
}
}
let mut rodata_name = None;
if self.fn_section_sizes[InputSection::Rodata] > 0 {
if state.pascal {
return self.fail_without_line(".rodata isn't supported with Pascal for now");
}
let new_name = state.make_name("rodata");
src[self.num_lines] += &format!(
" const char {}[{}] = {{1}};",
new_name,
self.fn_section_sizes[InputSection::Rodata]
);
rodata_name = Some(new_name);
}
let mut data_name = None;
if self.fn_section_sizes[InputSection::Data] > 0 {
let new_name = state.make_name("data");
let line = if state.pascal {
format!(
" var {}: packed array[1..{}] of char := [otherwise: 0];",
new_name,
self.fn_section_sizes[InputSection::Data]
)
} else {
format!(
" char {}[{}] = {{1}};",
new_name,
self.fn_section_sizes[InputSection::Data]
)
};
src[self.num_lines] += &line;
data_name = Some(new_name);
}
let mut bss_name = None;
if self.fn_section_sizes[InputSection::Bss] > 0 {
let new_name = state.make_name("bss");
if state.pascal {
return self.fail_without_line(".bss isn't supported with Pascal for now");
}
src[self.num_lines] += &format!(
" char {}[{}];",
new_name,
self.fn_section_sizes[InputSection::Bss]
);
bss_name = Some(new_name);
}
let mut data = EnumMap::default();
data[OutputSection::Text] = (text_name, self.fn_section_sizes[InputSection::Text]);
data[OutputSection::Data] = (data_name, self.fn_section_sizes[InputSection::Data]);
data[OutputSection::Rodata] = (rodata_name, self.fn_section_sizes[InputSection::Rodata]);
data[OutputSection::Bss] = (bss_name, self.fn_section_sizes[InputSection::Bss]);
let ret_fn = Function {
text_glabels: self.text_glabels.clone(),
asm_conts: self.asm_conts.clone(),
late_rodata_dummy_bytes,
jtbl_rodata_size,
late_rodata_asm_conts: self.late_rodata_asm_conts.clone(),
fn_desc: self.fn_desc.clone(),
data,
};
Ok((src, ret_fn))
}
}
/// Convert a float string to its hexadecimal representation
fn repl_float_hex(cap: &regex_lite::Captures) -> String {
let float_str = cap[0].trim().trim_end_matches('f');
let float_val = float_str.parse::<f32>().unwrap();
let hex_val = f32::to_be_bytes(float_val);
format!("{}", u32::from_be_bytes(hex_val))
}
pub(crate) struct ParseSourceResult {
pub functions: Vec<Function>,
pub deps: Vec<String>,
pub output: Vec<u8>,
}
pub(crate) fn parse_source(
infile_path: &Path,
args: &AsmProcArgs,
opts: &CompileOpts,
encode: bool,
) -> Result<ParseSourceResult> {
let (mut min_instr_count, mut skip_instr_count) = match opts.opt {
OptLevel::O0 => match opts.framepointer {
true => (8, 8),
false => (4, 4),
},
OptLevel::O1 | OptLevel::O2 => match opts.framepointer {
true => (6, 5),
false => (2, 1),
},
OptLevel::G => match opts.framepointer {
true => (7, 7),
false => (4, 4),
},
OptLevel::G3 => match opts.framepointer {
true => (4, 4),
false => (2, 2),
},
};
let mut prelude_if_late_rodata = 0;
if opts.kpic {
// Without optimizations, the PIC prelude always takes up 3 instructions.
// With optimizations, the prelude is optimized out if there's no late rodata.
if matches!(opts.opt, OptLevel::O2 | OptLevel::G3) {
prelude_if_late_rodata = 3;
} else {
min_instr_count += 3;
skip_instr_count += 3;
}
}
let use_jtbl_for_rodata =
matches!(opts.opt, OptLevel::O2 | OptLevel::G3) && !opts.framepointer && !opts.kpic;
let mut state = GlobalState::new(
min_instr_count,
skip_instr_count,
use_jtbl_for_rodata,
prelude_if_late_rodata,
opts.mips1,
opts.pascal,
);
let input_enc = &args.input_enc;
let output_enc = &args.output_enc;
let mut global_asm: Option<(GlobalAsmBlock, usize)> = None;
let mut asm_functions: Vec<Function> = vec![];
let mut output_lines: Vec<String> = vec![format!("#line 1 \"{}\"", infile_path.display())];
let mut deps: Vec<String> = vec![];
let mut is_cutscene_data = false;
let mut is_early_include = false;
let cutscene_re = Regex::new(r"CutsceneData (.|\n)*\[\] = \{")?;
let float_re = Regex::new(r"[-+]?[0-9]*\.?[0-9]+([eE][-+]?[0-9]+)?f")?;
let file_contents = fs::read(infile_path)?;
for (line_no, line) in input_enc.decode(&file_contents)?.lines().enumerate() {
let line_no = line_no + 1;
let mut raw_line = line.trim().to_owned();
let line = raw_line.trim_start();
// Print exactly one output line per source line, to make compiler
// errors have correct line numbers. These will be overridden with
// reasonable content further down.
output_lines.push("".to_owned());
if let Some((ref mut gasm, start_index)) = global_asm {
if line.starts_with(')') {
let (mut src, fun) = gasm.finish(&mut state)?;
if state.pascal {
// Pascal has a 1600-character line length limit, so some
// of the lines we emit may be broken up. Correct for that
// using a #line directive.
*src.last_mut().unwrap() += &format!("\n#line {}", line_no + 1);
}
for (i, line2) in src.iter().enumerate() {
output_lines[start_index + i] = line2.clone();
}
asm_functions.push(fun);
global_asm = None;
} else {
gasm.process_line(&raw_line, output_enc)?;
}
} else if line == "GLOBAL_ASM(" || line == "#pragma GLOBAL_ASM(" {
global_asm = Some((
GlobalAsmBlock::new(format!("GLOBAL_ASM block at line {}", &line_no.to_string())),
output_lines.len(),
));
} else if ((line.starts_with("GLOBAL_ASM(\"") || line.starts_with("#pragma GLOBAL_ASM(\""))
&& line.ends_with("\")"))
|| ((line.starts_with("INCLUDE_ASM(\"") || line.starts_with("INCLUDE_RODATA(\""))
&& line.contains("\",")
&& line.ends_with(");"))
{
let (prologue, fname) = if line.starts_with("INCLUDE_") {
// INCLUDE_ASM("path/to", functionname);
let (before, after) = line.split_once("\",").unwrap();
let fname = format!(
"{}/{}.s",
before[before.find('(').unwrap() + 2..].to_owned(),
after.trim()[..after.len() - 3].trim()
);
if line.starts_with("INCLUDE_RODATA") {
(vec![".section .rodata".to_string()], fname)
} else {
(vec![], fname)
}
} else {
// GLOBAL_ASM("path/to/file.s")
let fname = line[line.find('(').unwrap() + 2..line.len() - 2].to_string();
(vec![], fname)
};
let mut gasm = GlobalAsmBlock::new(fname.clone());
for line2 in prologue {
gasm.process_line(line2.trim_end(), output_enc)?;
}
if !Path::new(&fname).exists() {
// The GLOBAL_ASM block might be surrounded by an ifdef, so it's
// not clear whether a missing file actually represents a compile
// error. Pass the responsibility for determining that on to the
// compiler by emitting a bad include directive. (IDO treats
// #error as a warning for some reason.)
let output_lines_len = output_lines.len();
output_lines[output_lines_len - 1] = format!("#include \"GLOBAL_ASM:{}\"", fname);
continue;
}
let file_contents = fs::read(&fname)?;
for line2 in input_enc.decode(&file_contents)?.lines() {
gasm.process_line(line2.trim_end(), output_enc)?;
}
let (mut src, fun) = gasm.finish(&mut state)?;
let output_lines_len = output_lines.len();
if state.pascal {
// Pascal has a 1600-character line length limit, so avoid putting
// everything on the same line.
src.push(format!("#line {}", line_no + 1));
output_lines[output_lines_len - 1] = src.join("\n");
} else {
output_lines[output_lines_len - 1] = src.join("");
}
asm_functions.push(fun);
deps.push(fname);
} else if line == "#pragma asmproc recurse" {
// C includes qualified as
// #pragma asmproc recurse
// #include "file.c"
// will be processed recursively when encountered
is_early_include = true;
} else if is_early_include {
// Previous line was a #pragma asmproc recurse
is_early_include = false;
if !line.starts_with("#include ") {
return Err(anyhow::anyhow!(
"#pragma asmproc recurse must be followed by an #include "
));
}
let fpath = infile_path.parent().unwrap();
let fname = fpath.join(line[line.find(' ').unwrap() + 2..].trim());
deps.push(fname.to_str().unwrap().to_string());
let mut res = parse_source(&fname, args, opts, false)?;
deps.append(&mut res.deps);
let res_str = format!(
"{}#line {} \"{}\"",
String::from_utf8(res.output).expect("nested calls generate utf-8"),
line_no + 1,
infile_path.file_name().unwrap().to_str().unwrap()
);
let output_lines_len = output_lines.len();
output_lines[output_lines_len - 1] = res_str;
} else {
if args.encode_cutscene_data_floats {
// This is a hack to replace all floating-point numbers in an array of a particular type
// (in this case CutsceneData) with their corresponding IEEE-754 hexadecimal representation
if cutscene_re.is_match(line) {
is_cutscene_data = true;
} else if line.ends_with("};") {
is_cutscene_data = false;
}
if is_cutscene_data {
raw_line = float_re.replace_all(&raw_line, repl_float_hex).into_owned();
}
}
let output_lines_len = output_lines.len();
output_lines[output_lines_len - 1] = raw_line.to_owned();
}
}
let out_data = if encode {
let newline_encoded = output_enc.encode("\n")?;
let mut data = vec![];
for line in output_lines {
let line_encoded = output_enc.encode(&line)?;
data.write_all(&line_encoded)?;
data.write_all(&newline_encoded)?;
}
data
} else {
let str = format!("{}\n", output_lines.join("\n"));
str.as_bytes().to_vec()
};
Ok(ParseSourceResult {
functions: asm_functions,
deps,
output: out_data,
})
}