use crate::assemble::Stage;
use std::path::Path;
#[derive(Debug, Clone)]
pub struct Rule {
pub ns: String,
pub verb: Option<String>,
pub args: Vec<String>,
pub vertex: Option<String>,
pub fragment: Option<String>,
pub compute: Option<String>,
pub any: Option<String>,
}
impl Rule {
fn template(&self, stage: Stage) -> Option<&str> {
let specific = match stage {
Stage::Vertex => &self.vertex,
Stage::Fragment => &self.fragment,
Stage::Compute => &self.compute,
};
specific.as_deref().or(self.any.as_deref())
}
}
#[derive(Debug, Clone, Default)]
pub struct Dialect {
#[allow(dead_code)]
pub name: String,
pub prelude_vertex: Option<String>,
pub prelude_fragment: Option<String>,
pub prelude_any: Option<String>,
pub prelude_extra: Option<String>,
pub epilogue: Option<String>,
pub prelude_files_vertex: Vec<String>,
pub prelude_files_fragment: Vec<String>,
pub define_names: Vec<String>,
pub discover_defines: bool,
pub rules: Vec<Rule>,
pub deck: bool,
}
impl Dialect {
pub fn prelude(&self, stage: Stage) -> Option<&str> {
let specific = match stage {
Stage::Vertex => &self.prelude_vertex,
Stage::Fragment => &self.prelude_fragment,
Stage::Compute => &None,
};
specific.as_deref().or(self.prelude_any.as_deref())
}
pub fn prelude_files(&self, stage: Stage) -> &[String] {
match stage {
Stage::Vertex => &self.prelude_files_vertex,
Stage::Fragment => &self.prelude_files_fragment,
Stage::Compute => &[],
}
}
pub fn expand_line(&self, line: &str, stage: Stage) -> Option<Vec<String>> {
let (ns, rest) = parse_pragma(line)?;
for rule in &self.rules {
if rule.ns != ns {
continue;
}
let mut tokens = rest.split_whitespace();
if let Some(verb) = &rule.verb {
match tokens.next() {
Some(t) if t == verb => {}
_ => continue,
}
}
let values: Vec<&str> = tokens.collect();
if values.len() != rule.args.len() {
continue;
}
let template = rule.template(stage)?;
return Some(render(template, &rule.args, &values));
}
None
}
}
fn parse_pragma(line: &str) -> Option<(String, &str)> {
let trimmed = line.trim_start();
let after = trimmed.strip_prefix("#pragma")?;
if !after.starts_with([' ', '\t']) {
return None;
}
let (ns, rest) = after.split_once(':')?;
let ns = ns.trim();
if ns.is_empty() {
return None;
}
Some((ns.to_string(), rest))
}
fn render(template: &str, args: &[String], values: &[&str]) -> Vec<String> {
let mut order: Vec<usize> = (0..args.len()).collect();
order.sort_by_key(|&i| std::cmp::Reverse(args[i].len()));
let mut out = template.to_string();
for i in order {
out = out.replace(&format!("{{{}}}", args[i]), values[i]);
}
out.lines().map(str::to_string).collect()
}
pub fn has_main(source: &str) -> bool {
source
.lines()
.any(|l| l.replace(char::is_whitespace, "").contains("voidmain("))
}
#[derive(Debug, Clone)]
pub struct Preference {
pub preset: Option<String>,
pub auto: bool,
pub custom_rules: Vec<Rule>,
pub custom_prelude: Option<String>,
}
impl Default for Preference {
fn default() -> Self {
Preference {
preset: None,
auto: true,
custom_rules: Vec::new(),
custom_prelude: None,
}
}
}
pub fn resolve(pref: &Preference, source: &str, dir: &Path) -> Option<Dialect> {
let base = match pref.preset.as_deref() {
Some(name) => crate::preset::by_name(name, dir),
None if pref.auto => crate::preset::detect(source, dir),
None => None,
};
let has_custom = !pref.custom_rules.is_empty() || pref.custom_prelude.is_some();
if base.is_none() && !has_custom {
return None;
}
let mut d = base.unwrap_or_else(|| Dialect {
name: "custom".into(),
deck: false,
..Default::default()
});
if !pref.custom_rules.is_empty() {
let mut rules = pref.custom_rules.clone();
rules.extend(std::mem::take(&mut d.rules));
d.rules = rules;
}
if pref.custom_prelude.is_some() {
d.prelude_extra = pref.custom_prelude.clone();
}
Some(d)
}
#[cfg(test)]
#[allow(clippy::unwrap_used)] mod tests {
use super::*;
#[test]
fn parse_pragma_splits_namespace_and_body() {
assert_eq!(
parse_pragma("#pragma maplibre: define lowp float opacity"),
Some(("maplibre".to_string(), " define lowp float opacity"))
);
assert_eq!(
parse_pragma(" #pragma glslify : foo").map(|(n, _)| n),
Some("glslify".to_string())
);
assert_eq!(parse_pragma("#version 300 es"), None);
assert_eq!(parse_pragma("#pragma optimize(on)"), None);
assert_eq!(parse_pragma("#pragmatic: x"), None);
}
fn engine_dialect() -> Dialect {
let rule = |verb: &str, vtx: &str, frag: Option<&str>| Rule {
ns: "mx".into(),
verb: Some(verb.into()),
args: vec!["prec".into(), "type".into(), "name".into()],
vertex: Some(vtx.into()),
fragment: frag.map(str::to_string),
compute: None,
any: None,
};
Dialect {
name: "test".into(),
rules: vec![
rule(
"define",
"in {prec} {type} a_{name};\nout {prec} {type} {name};",
Some("in {prec} {type} {name};"),
),
rule("initialize", "{name} = a_{name};", None),
],
..Default::default()
}
}
#[test]
fn define_expands_per_stage() {
let d = engine_dialect();
let v = d
.expand_line("#pragma mx: define lowp float opacity", Stage::Vertex)
.unwrap();
assert!(v.iter().any(|l| l == "in lowp float a_opacity;"));
assert!(v.iter().any(|l| l == "out lowp float opacity;"));
let f = d
.expand_line("#pragma mx: define lowp float opacity", Stage::Fragment)
.unwrap();
assert!(f.iter().any(|l| l == "in lowp float opacity;"));
assert!(!f.iter().any(|l| l.contains("a_opacity")));
}
#[test]
fn initialize_binds_the_name() {
let v = engine_dialect()
.expand_line(
" #pragma mx: initialize lowp float opacity",
Stage::Vertex,
)
.unwrap();
assert!(v.iter().any(|l| l.contains("opacity = a_opacity;")));
}
#[test]
fn non_matching_lines_are_left_verbatim() {
let d = engine_dialect();
assert!(
d.expand_line("uniform vec2 u_translation;", Stage::Vertex)
.is_none()
);
assert!(
d.expand_line("#pragma mx: whoknows x", Stage::Vertex)
.is_none()
);
assert!(
d.expand_line("#pragma mx: define float x", Stage::Vertex)
.is_none()
);
}
#[test]
fn render_substitutes_all_occurrences() {
let out = render(
"uniform lowp float u_{name}_t;\nin {prec} {type} a_{name};",
&["prec".into(), "type".into(), "name".into()],
&["lowp", "float", "opacity"],
);
assert_eq!(out[0], "uniform lowp float u_opacity_t;");
assert_eq!(out[1], "in lowp float a_opacity;");
}
#[test]
fn has_main_detects_the_entry_point() {
assert!(has_main("void main() { }"));
assert!(has_main("void main(){}"));
assert!(!has_main("void mainImage(out vec4 c, in vec2 p){}"));
}
}