use super::DebugDirectiveOverflow;
use super::MMixAssembler;
use crate::mmix::TrapCode;
use std::path::Path;
use std::path::PathBuf;
use tracing::debug;
const BYTE_ORDER_MARK: char = '\u{FEFF}';
impl MMixAssembler {
pub(super) fn strip_byte_order_mark(source: &str) -> &str {
source.strip_prefix(BYTE_ORDER_MARK).unwrap_or(source)
}
fn is_blank_segment(segment: &str, start_line: usize) -> bool {
let text = if start_line == 1 {
Self::strip_byte_order_mark(segment)
} else {
segment
};
text.trim().is_empty()
}
pub(super) fn blank_whole_line_comments(source: &str) -> String {
let mut result = String::with_capacity(source.len());
for line in source.split_inclusive('\n') {
let content = line.strip_suffix('\n').unwrap_or(line);
let is_comment_line = match content.chars().next() {
Some(' ') | Some('\t') | None => false,
Some(c) => !(c.is_ascii_alphanumeric() || c == ':' || c == '_'),
};
if is_comment_line {
if content.len() != line.len() {
result.push('\n');
}
} else {
result.push_str(line);
}
}
result
}
pub(super) fn preprocess_debug(
source: &str,
filename: &str,
start_index: usize,
) -> (String, Vec<Vec<u8>>, DebugDirectiveOverflow) {
let mut result = String::new();
let mut strings = Vec::new();
let mut overflow = None;
for (index, line) in source.lines().enumerate() {
match match_debug_line(line) {
Some((label, text)) => {
let k = start_index + strings.len();
if k > 255 {
if overflow.is_none() {
let col = label.map_or(0, |l| l.chars().count()) + 1;
overflow = Some((filename.to_string(), index + 1, col));
}
} else {
result.push_str(&debug_trap_line(label.unwrap_or(""), k));
}
strings.push(text.as_bytes().to_vec());
}
None => {
result.push_str(line);
result.push('\n');
}
}
}
debug!("Preprocessed source:\n{}", result);
(result, strings, overflow)
}
pub fn resolve_includes<R>(
root_source: &str,
root_filename: &str,
base_dir: &Path,
read: &R,
) -> Result<Vec<(String, String)>, String>
where
R: Fn(&Path) -> std::io::Result<String>,
{
let mut chain = Vec::new();
Self::resolve_includes_chain(root_source, root_filename, base_dir, read, &mut chain)
}
fn resolve_includes_chain<R>(
root_source: &str,
root_filename: &str,
base_dir: &Path,
read: &R,
chain: &mut Vec<PathBuf>,
) -> Result<Vec<(String, String)>, String>
where
R: Fn(&Path) -> std::io::Result<String>,
{
let mut units = Vec::new();
let mut segment = String::new();
let mut segment_start_line: usize = 1;
let mut current_line: usize = 0;
for raw_line in root_source.split_inclusive('\n') {
current_line += 1;
let content = raw_line.strip_suffix('\n').unwrap_or(raw_line);
let content = if current_line == 1 {
Self::strip_byte_order_mark(content)
} else {
content
};
let Some(operand) = Self::parse_include_operand(content) else {
segment.push_str(raw_line);
continue;
};
if !Self::is_blank_segment(&segment, segment_start_line) {
units.push((
root_filename.to_string(),
Self::pad_source(&segment, segment_start_line),
));
}
segment.clear();
let target = Self::normalize_lexically(&base_dir.join(&operand));
let include_col = content.chars().take_while(|c| c.is_whitespace()).count() + 1;
if chain.contains(&target) {
let mut names: Vec<String> =
chain.iter().map(|p| p.display().to_string()).collect();
names.push(target.display().to_string());
return Err(format!(
"{root_filename}:{current_line}:{include_col}: include cycle detected: {}",
names.join(" -> ")
));
}
let included_source = read(&target).map_err(|err| {
format!(
"{root_filename}:{current_line}:{include_col}: cannot read included file '{}': {}",
target.display(),
err
)
})?;
let included_filename = target.display().to_string();
let included_base_dir = target
.parent()
.map(Path::to_path_buf)
.unwrap_or_else(|| PathBuf::from(""));
chain.push(target);
let included_units = Self::resolve_includes_chain(
&included_source,
&included_filename,
&included_base_dir,
read,
chain,
)?;
chain.pop();
units.extend(included_units);
segment_start_line = current_line + 1;
}
if !Self::is_blank_segment(&segment, segment_start_line) {
units.push((
root_filename.to_string(),
Self::pad_source(&segment, segment_start_line),
));
}
Ok(units)
}
fn parse_include_operand(line: &str) -> Option<String> {
let without_comment = match line.find('%') {
Some(idx) => &line[..idx],
None => line,
};
let trimmed = without_comment.trim();
let mut parts = trimmed.splitn(2, |c: char| c.is_whitespace());
let keyword = parts.next()?;
if keyword != "INCLUDE" {
return None;
}
let operand = parts.next().unwrap_or("").trim();
if operand.is_empty() {
return None;
}
let unquoted = if operand.len() >= 2 && operand.starts_with('"') && operand.ends_with('"') {
&operand[1..operand.len() - 1]
} else {
operand
};
Some(unquoted.to_string())
}
fn pad_source(text: &str, start_line: usize) -> String {
let padding = start_line.saturating_sub(1);
let mut padded = String::with_capacity(text.len() + padding);
for _ in 0..padding {
padded.push('\n');
}
padded.push_str(text);
padded
}
fn normalize_lexically(path: &Path) -> PathBuf {
use std::path::Component;
let mut result = PathBuf::new();
for component in path.components() {
match component {
Component::CurDir => {}
Component::ParentDir => {
if !result.pop() {
result.push("..");
}
}
other => result.push(other.as_os_str()),
}
}
result
}
}
fn debug_trap_line(prefix: &str, k: usize) -> String {
let mut line: String = prefix
.chars()
.map(|c| if c.is_whitespace() { ' ' } else { c })
.collect();
if line.is_empty() {
line.push(' ');
}
let code = TrapCode::Debug as u8;
format!("{line}TRAP\t0,#{code:02X},{k}\n")
}
fn match_debug_line(line: &str) -> Option<(Option<&str>, &str)> {
if let Some((label_end, text)) = match_labeled_debug(line) {
return Some((Some(&line[..label_end]), text));
}
let text = match_debug_after(line, 0)?;
Some((None, text))
}
fn match_labeled_debug(line: &str) -> Option<(usize, &str)> {
let ws_start = line.char_indices().find(|(_, c)| c.is_whitespace())?.0;
let label_end = line[ws_start..]
.char_indices()
.find(|(_, c)| !c.is_whitespace())
.map_or(line.len(), |(i, _)| ws_start + i);
let text = match_debug_after(line, label_end)?;
Some((label_end, text))
}
fn match_debug_after(line: &str, start: usize) -> Option<&str> {
let rest = line.get(start..)?.strip_prefix("debug")?;
let ws_end = rest
.char_indices()
.find(|(_, c)| !c.is_whitespace())
.map_or(rest.len(), |(i, _)| i);
if ws_end == 0 {
return None;
}
let after_quote = rest[ws_end..].strip_prefix('"')?;
let close = after_quote.find('"')?;
if after_quote[close + 1..].chars().any(|c| !c.is_whitespace()) {
return None;
}
Some(&after_quote[..close])
}