use std::{
collections::HashSet,
path::{Path, PathBuf},
};
use crate::{
Diagnostic, Error, ModulePath, SyntaxUtil, resolve::CodegenPkg, validate::validate_wesl,
};
use quote::{format_ident, quote};
use wgsl_parse::{
lexer::{Lexer, Token},
syntax::TranslationUnit,
};
use wgsl_types::idents::RESERVED_WORDS;
fn is_mod_ident(name: &str) -> bool {
let mut lex = Lexer::new(name);
RESERVED_WORDS.contains(&name)
|| matches!(
(lex.next(), lex.next()),
(Some(Ok((_, Token::Ident(_), _))), None)
)
}
const RESERVED_MOD_NAMES: &[&str] = &[
"const_assert",
"continue",
"continuing",
"default",
"diagnostic",
"discard",
"else",
"enable",
"false",
"fn",
"for",
"if",
"let",
"loop",
"override",
"requires",
"return",
"struct",
"switch",
"true",
"var",
"while",
"self",
"super",
"package",
"as",
"import",
];
pub struct PkgBuilder {
name: String,
dependencies: Vec<&'static CodegenPkg>,
}
pub struct Pkg {
pub crate_name: String,
pub root: Module,
pub dependencies: Vec<&'static CodegenPkg>,
}
#[derive(Debug)]
pub struct Module {
pub name: String,
pub source: String,
pub submodules: Vec<Module>,
}
#[derive(Debug, thiserror::Error)]
pub enum ScanDirectoryError {
#[error("Package root was not found: `{0}`")]
RootNotFound(PathBuf),
#[error("Module name `{0}` is reserved")]
ReservedModName(String),
#[error("I/O error while scanning package root: {0}")]
Io(#[from] std::io::Error),
}
impl PkgBuilder {
pub fn new(name: &str) -> Self {
Self {
name: name.replace('-', "_"),
dependencies: Vec::new(),
}
}
pub fn add_package(mut self, pkg: &'static CodegenPkg) -> Self {
self.dependencies.push(pkg);
self
}
pub fn add_packages(mut self, pkgs: impl IntoIterator<Item = &'static CodegenPkg>) -> Self {
for pkg in pkgs {
self = self.add_package(pkg);
}
self
}
pub fn scan_root(self, path: impl AsRef<Path>) -> Result<Pkg, ScanDirectoryError> {
fn process_path(path: &Path) -> Result<Option<Module>, ScanDirectoryError> {
let path_with_ext_wesl = path.with_extension("wesl");
let path_with_ext_wgsl = path.with_extension("wgsl");
let path_without_ext = path.with_extension("");
let path_filename = path_without_ext
.file_stem()
.unwrap()
.to_string_lossy()
.to_string();
if !path_with_ext_wesl.is_file()
&& !path_with_ext_wgsl.is_file()
&& !path_without_ext.is_dir()
{
return Ok(None);
}
if RESERVED_MOD_NAMES.contains(&path_filename.as_str()) {
return Err(ScanDirectoryError::ReservedModName(path_filename));
}
if !is_mod_ident(&path_filename) {
println!(
"cargo::warning=skipped file/dir: not a WGSL ident `{path_filename}` {path:?}"
);
return Ok(None);
}
let source = if path_with_ext_wesl.is_file() {
std::fs::read_to_string(&path_with_ext_wesl)?
} else if path_with_ext_wgsl.is_file() {
std::fs::read_to_string(&path_with_ext_wgsl)?
} else {
String::new()
};
let mut submodules = Vec::new();
if path_without_ext.is_dir() {
let mut unique_submodules = HashSet::new();
for entry in std::fs::read_dir(&path_without_ext)? {
let Ok(entry) = entry else { continue };
let submodule_path = entry.path().with_extension("");
unique_submodules.insert(submodule_path);
}
for entry in unique_submodules {
match process_path(&entry) {
Ok(Some(module)) => submodules.push(module),
Ok(None) => {}
Err(err) => {
println!("cargo::error=error processing submodule {entry:?}: {err}")
}
}
}
};
if source.is_empty() && submodules.is_empty() {
return Ok(None);
}
let module = Module {
name: path_filename,
source,
submodules,
};
Ok(Some(module))
}
let root_path = path.as_ref().to_path_buf();
let potential_module = process_path(&root_path)?;
let Some(mut module) = potential_module else {
return Err(ScanDirectoryError::RootNotFound(root_path));
};
module.name = self.name;
let crate_name = std::env::var("CARGO_PKG_NAME")
.expect("CARGO_PKG_NAME environment variable is not defined")
.to_string();
Ok(Pkg {
crate_name,
root: module,
dependencies: self.dependencies,
})
}
}
impl Module {
fn codegen(&self) -> proc_macro2::TokenStream {
let mod_ident = format_ident!("r#{}", self.name);
let name = &self.name;
let source = &self.source;
let submodules = self.submodules.iter().map(|submod| {
let name = &submod.name;
let ident = format_ident!("r#{}", name);
quote! { &#ident::MODULE }
});
let submods = self.submodules.iter().map(|submod| submod.codegen());
quote! {
pub mod #mod_ident {
use super::CodegenModule;
pub const MODULE: CodegenModule = CodegenModule {
name: #name,
source: #source,
submodules: &[#(#submodules),*]
};
#(#submods)*
}
}
}
fn validate(&self, parent_path: ModulePath) -> Result<(), Error> {
let mut path = parent_path.clone();
path.push(&self.name);
eprintln!("INFO: validate {path}");
let to_diagnostic = |e: Error| {
Diagnostic::from(e)
.with_module_path(path.clone(), None)
.with_source(self.source.clone())
};
let mut wesl: TranslationUnit = self
.source
.parse()
.map_err(|e: wgsl_parse::Error| to_diagnostic(e.into()))?;
wesl.retarget_idents();
validate_wesl(&wesl).map_err(|e| to_diagnostic(e.into()))?;
for module in &self.submodules {
module.validate(path.clone())?;
}
Ok(())
}
}
impl Pkg {
pub fn codegen(&self) -> String {
let deps = self.dependencies.iter().map(|dep| {
let crate_name = format_ident!("r#{}", dep.crate_name);
let mod_name = format_ident!("r#{}", dep.root.name);
quote! { &#crate_name::#mod_name::PACKAGE }
});
let crate_name = &self.crate_name;
let root_name = &self.root.name;
let root_source = &self.root.source;
let submodules = self.root.submodules.iter().map(|submod| {
let name = &submod.name;
let ident = format_ident!("r#{}", name);
quote! { &#ident::MODULE }
});
let submods = self.root.submodules.iter().map(|submod| submod.codegen());
let tokens = quote! {
pub const PACKAGE: CodegenPkg = CodegenPkg {
crate_name: #crate_name,
root: &MODULE,
dependencies: &[#(#deps),*],
};
pub const MODULE: CodegenModule = CodegenModule {
name: #root_name,
source: #root_source,
submodules: &[#(#submodules),*]
};
#(#submods)*
};
tokens.to_string()
}
pub fn validate(self) -> Result<Self, Error> {
self.root.validate(ModulePath::new_root())?;
Ok(self)
}
pub fn build_artifact(&self) -> std::io::Result<()> {
let code = self.codegen();
let out_dir = std::path::Path::new(
&std::env::var_os("OUT_DIR").expect("OUT_DIR environment variable is not defined"),
)
.join(format!("{}.rs", self.root.name));
std::fs::write(&out_dir, code)?;
Ok(())
}
}