diff options
Diffstat (limited to 'tools/asm-processor/rust/src/preprocess.rs')
| -rw-r--r-- | tools/asm-processor/rust/src/preprocess.rs | 969 |
1 files changed, 969 insertions, 0 deletions
diff --git a/tools/asm-processor/rust/src/preprocess.rs b/tools/asm-processor/rust/src/preprocess.rs new file mode 100644 index 0000000..73af653 --- /dev/null +++ b/tools/asm-processor/rust/src/preprocess.rs @@ -0,0 +1,969 @@ +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("alabel ") + || line.starts_with("endlabel ") + || line.starts_with("enddlabel ") + || line.starts_with("nonmatching ") + || (!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(',') + .next_back() + .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_n("".to_owned(), 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, + }) +} |
