mirror of
https://gitlab.com/kholdfuzion/goldeneye_src
synced 2026-06-24 00:11:26 -04:00
Everybody loves a good yearly or so update
This commit is contained in:
@@ -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
@@ -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: ®ex_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: ®ex_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,
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user