use std::{
collections::{HashMap, HashSet},
fs::File,
io::{self, BufRead, BufReader},
path::{Path, PathBuf},
};
use lazy_static::lazy_static;
use regex::Regex;
#[derive(Debug)]
pub enum PreprocessorError {
FileNotFound(String),
FileNotValidUtf8(String),
UnknownDirective(String),
IncludeIncorrectArgs,
MacroNoParenthesis,
MacroIncorrectArgs(usize, usize),
}
lazy_static! {
static ref REGEX_ID: Regex =
Regex::new(r"([_\p{XID_Start}][\p{XID_Continue}]+)|([\p{XID_Start}])").unwrap();
static ref REGEX_DEFINE_MACRO: Regex = Regex::new(r"((?:[_\p{XID_Start}][\p{XID_Continue}]+)|(?:[\p{XID_Start}]))\(((?:[_\p{XID_Start}][\p{XID_Continue}]*(?:,\s*)*)+)\)\s+(.*)").unwrap();
static ref REGEX_BLOCK_COMMENT: Regex = Regex::new(r"/\*.*?\*/").unwrap();
}
fn _remove_comments(line: &mut String, in_block_comment: bool) -> bool {
if in_block_comment {
if let Some(closing_idx) = line.find("*/") {
line.replace_range(0..closing_idx + 2, "")
} else {
return in_block_comment;
}
}
while let Some(block_comment) = REGEX_BLOCK_COMMENT.find(line) {
line.replace_range(block_comment.start()..block_comment.end(), "");
}
if let Some(inline_comment_idx) = line.find("//") {
line.replace_range(inline_comment_idx.., "");
return false;
}
if line.contains("/*") {
return true;
}
false
}
enum DefineDirective {
Value(String),
Macro(Vec<String>, String),
}
fn _substitute_macros(
line: String,
defines: &HashMap<String, DefineDirective>,
) -> Result<(bool, String), PreprocessorError> {
let mut result = line.clone();
let mut i = 0;
while i < result.len() {
let id = REGEX_ID.find(&result[i..]);
if let Some(id) = id {
let id_start = i + id.start();
let id_end = i + id.end();
let id = id.as_str();
let id_len = id.len();
match defines.get(id) {
Some(DefineDirective::Value(value)) => {
result.replace_range(id_start..id_end, value);
}
Some(DefineDirective::Macro(args, body)) => {
if &result[id_end..id_end + 1] != "(" {
i += id_len;
continue;
}
let mut paren_count = 0;
let mut paren_idx = id_end;
let mut commas_idx: Vec<usize> = vec![];
while paren_idx < result.len() {
match &result[paren_idx..paren_idx + 1] {
"(" | "{" => paren_count += 1,
")" | "}" => {
paren_count -= 1;
if paren_count == 0 {
break;
}
}
"," => {
if paren_count == 1 {
commas_idx.push(paren_idx);
}
}
_ => {}
}
paren_idx += 1;
}
if paren_count != 0 {
i += id_len;
return Err(PreprocessorError::MacroNoParenthesis);
}
let mut arg_values: Vec<String> = vec![];
let mut arg_start = id_end + 1;
for comma_idx in commas_idx.iter() {
arg_values.push(result[arg_start..*comma_idx].trim().to_string());
arg_start = comma_idx + 1;
}
arg_values.push(result[arg_start..paren_idx].trim().to_string());
if arg_values.len() != args.len() {
i += id_len;
return Err(PreprocessorError::MacroIncorrectArgs(
args.len(),
arg_values.len(),
));
}
let mut arg_defines = HashMap::new();
for (arg_name, arg_value) in args.iter().zip(arg_values.iter()) {
arg_defines.insert(
arg_name.to_string(),
DefineDirective::Value(
_substitute_macros(arg_value.to_string(), defines)
.or_else(|_| Ok((false, arg_value.to_string())))?
.1,
),
);
}
let (_changed, new_body) = _substitute_macros(body.to_string(), &arg_defines)?;
result.replace_range(id_start..paren_idx + 1, &new_body);
}
_ => {}
}
i += id_len;
} else {
break;
}
}
Ok((result != line, result))
}
fn _preprocess(
filename: &str,
basepath: &Path,
visited: &mut HashSet<PathBuf>,
defines: &mut HashMap<String, DefineDirective>,
) -> Result<String, PreprocessorError> {
let source_path = basepath.join(filename);
let source_path_parent = PathBuf::from(source_path.parent().unwrap());
if visited.contains(&source_path) {
return Ok("".to_string());
}
visited.insert(source_path.clone());
let file = match File::open(source_path) {
Ok(f) => f,
Err(_) => return Err(PreprocessorError::FileNotFound(filename.to_string())),
};
let br = BufReader::new(file);
let mut contents = String::new();
let lines = br
.lines()
.collect::<Result<Vec<_>, io::Error>>()
.map_err(|_| PreprocessorError::FileNotValidUtf8(filename.to_string()))?;
let mut i = 0;
let mut in_block_comment = false;
loop {
if i >= lines.len() {
break;
}
let mut line = lines[i].to_string();
while line.ends_with('\\') {
line.pop();
i += 1;
if i >= lines.len() {
break;
}
line += &lines[i];
}
in_block_comment = _remove_comments(&mut line, in_block_comment);
if in_block_comment {
i += 1;
continue;
}
if let Some(directive_idx) = line.find('#') {
let mut directive_content = "".to_string();
let directive_line = &line[directive_idx..];
let directive_args = directive_line
.split(' ')
.filter(|arg| !arg.trim().is_empty())
.collect::<Vec<&str>>();
if directive_args[0] == "#include" {
if directive_args.len() != 2 {
return Err(PreprocessorError::IncludeIncorrectArgs);
}
let dest_path = directive_args[1];
if !((dest_path.starts_with('"') && dest_path.ends_with('"'))
|| (dest_path.starts_with('<') && dest_path.ends_with('>')))
{
return Err(PreprocessorError::IncludeIncorrectArgs);
}
let dest_path = &dest_path[1..dest_path.len() - 1];
let contents_to_add =
_preprocess(dest_path, &source_path_parent, visited, defines)?;
directive_content += &contents_to_add
} else if directive_args[0] == "#define" {
if directive_args.len() < 3 {
return Err(PreprocessorError::IncludeIncorrectArgs);
}
if let Some(caps) = REGEX_DEFINE_MACRO.captures(directive_line) {
let macro_name = caps.get(1).unwrap().as_str();
let macro_args = caps.get(2).unwrap().as_str();
let macro_body = caps.get(3).unwrap().as_str();
let macro_args = macro_args
.split(',')
.map(|arg| arg.trim().to_string())
.collect::<Vec<String>>();
defines.insert(
macro_name.to_string(),
DefineDirective::Macro(macro_args, macro_body.to_string()),
);
} else {
let var_name = directive_args[1];
let var_value = directive_args[2..].join(" ");
defines.insert(var_name.to_string(), DefineDirective::Value(var_value));
}
} else if directive_args[0] == "#undef" {
if directive_args.len() != 2 {
return Err(PreprocessorError::IncludeIncorrectArgs);
}
let var_name = directive_args[1];
defines.remove(var_name);
} else {
return Err(PreprocessorError::UnknownDirective(
directive_args[0].to_string(),
));
}
line.replace_range(directive_idx.., &directive_content);
};
loop {
let (changed, new_line) = _substitute_macros(line, defines)?;
line = new_line;
if !changed {
break;
}
}
contents.push_str(&line);
contents.push('\n');
i += 1;
}
Ok(contents)
}
pub fn preprocess(filename: &str, basepath: &Path) -> Result<String, PreprocessorError> {
_preprocess(
filename,
basepath,
&mut HashSet::new(), &mut HashMap::new(), )
}
#[cfg(test)]
mod tests {
use std::fs::read_dir;
use super::*;
#[test]
fn test_snapshot() {
let workspace_root = Path::new(env!("CARGO_WORKSPACE_DIR"));
let current_file = file!();
let snapshot_dir = Path::new(workspace_root)
.join(current_file)
.join("../../fixtures")
.canonicalize()
.unwrap();
for entry in read_dir(&snapshot_dir).unwrap() {
let entry = entry.unwrap();
let path = entry.path();
let filename = path.file_name().unwrap().to_str().unwrap().to_string();
if !filename.ends_with(".wgsl") {
continue;
}
println!("starting {}", filename);
let result = preprocess(&filename, &snapshot_dir);
if !result.is_ok() {
println!("{:?}", result);
}
assert!(result.is_ok(), "Failed to preprocess file: {}", filename);
insta::assert_snapshot!(filename, result.unwrap());
}
}
}