Theme

alex/volcanic

Automated vulkan bindings

BRANCH main
main
SNAPSHOT Improved loading api and made ready for publish
DOWNLOAD
Download file

root / codegen / src / select.rs

use crate::registry::{EnumEntry, Registry};
use indexmap::{IndexMap, IndexSet};

#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd)]
pub enum FeatureExpr {
    Feature(String),
    All(Vec<FeatureExpr>),
    Any(Vec<FeatureExpr>),
    Not(Box<FeatureExpr>),
    True,
    False,
}

impl FeatureExpr {
    pub fn feature(name: impl Into<String>) -> Self {
        Self::Feature(name.into())
    }

    pub fn all(exprs: impl IntoIterator<Item = Self>) -> Self {
        let mut out = Vec::new();
        for expr in exprs {
            match expr {
                Self::True => {}
                Self::False => return Self::False,
                Self::All(children) => out.extend(children),
                other => out.push(other),
            }
        }
        normalize_operands(&mut out);
        let snapshot = out.clone();
        out.retain(|expr| {
            !snapshot
                .iter()
                .any(|other| other != expr && implies(other, expr))
        });
        finish_operands(out, Self::All, Self::True)
    }

    pub fn any(exprs: impl IntoIterator<Item = Self>) -> Self {
        let mut out = Vec::new();
        for expr in exprs {
            match expr {
                Self::True => return Self::True,
                Self::False => {}
                Self::Any(children) => out.extend(children),
                other => out.push(other),
            }
        }
        normalize_operands(&mut out);
        let snapshot = out.clone();
        out.retain(|expr| {
            !snapshot
                .iter()
                .any(|other| other != expr && implies(expr, other))
        });
        finish_operands(out, Self::Any, Self::False)
    }

    pub fn not(expr: Self) -> Self {
        match expr {
            Self::True => Self::False,
            Self::False => Self::True,
            Self::Not(inner) => *inner,
            other => Self::Not(Box::new(other)),
        }
    }

    pub fn cfg_expr(&self) -> String {
        match self {
            Self::Feature(feature) => format!("feature = \"{feature}\""),
            Self::All(exprs) => format!(
                "all({})",
                exprs
                    .iter()
                    .map(Self::cfg_expr)
                    .collect::<Vec<_>>()
                    .join(", ")
            ),
            Self::Any(exprs) => format!(
                "any({})",
                exprs
                    .iter()
                    .map(Self::cfg_expr)
                    .collect::<Vec<_>>()
                    .join(", ")
            ),
            Self::Not(expr) => format!("not({})", expr.cfg_expr()),
            Self::True => "all()".to_string(),
            Self::False => "any()".to_string(),
        }
    }

    pub fn description(&self) -> String {
        match self {
            Self::Feature(feature) => format!("feature `{feature}`"),
            Self::All(exprs) => exprs
                .iter()
                .map(description_part)
                .collect::<Vec<_>>()
                .join(" and "),
            Self::Any(exprs) => exprs
                .iter()
                .map(description_part)
                .collect::<Vec<_>>()
                .join(" or "),
            Self::Not(expr) => format!("not {}", description_part(expr)),
            Self::True => "no additional features".to_string(),
            Self::False => "an unavailable feature requirement".to_string(),
        }
    }

    pub fn is_false(&self) -> bool {
        *self == Self::False
    }

    pub fn implies(&self, other: &Self) -> bool {
        implies(self, other)
    }
}

fn normalize_operands(exprs: &mut Vec<FeatureExpr>) {
    exprs.sort();
    exprs.dedup();
}

fn finish_operands(
    mut exprs: Vec<FeatureExpr>,
    combine: impl FnOnce(Vec<FeatureExpr>) -> FeatureExpr,
    identity: FeatureExpr,
) -> FeatureExpr {
    match exprs.len() {
        0 => identity,
        1 => exprs.pop().unwrap(),
        _ => combine(exprs),
    }
}

fn implies(left: &FeatureExpr, right: &FeatureExpr) -> bool {
    if left == right || *right == FeatureExpr::True || *left == FeatureExpr::False {
        return true;
    }
    match (left, right) {
        (FeatureExpr::Feature(left), FeatureExpr::Feature(right)) => feature_implies(left, right),
        (FeatureExpr::All(left), _) => left.iter().any(|expr| implies(expr, right)),
        (FeatureExpr::Any(left), _) => left.iter().all(|expr| implies(expr, right)),
        (_, FeatureExpr::All(right)) => right.iter().all(|expr| implies(left, expr)),
        (_, FeatureExpr::Any(right)) => right.iter().any(|expr| implies(left, expr)),
        (FeatureExpr::Not(left), FeatureExpr::Not(right)) => implies(right, left),
        _ => false,
    }
}

fn feature_implies(left: &str, right: &str) -> bool {
    left == right
        || match (core_feature_level(left), core_feature_level(right)) {
            (Some(left), Some(right)) => left >= right,
            _ => false,
        }
}

fn core_feature_level(feature: &str) -> Option<u8> {
    feature.strip_prefix("core_1_")?.parse().ok()
}

fn description_part(expr: &FeatureExpr) -> String {
    let description = expr.description();
    if matches!(
        expr,
        FeatureExpr::Feature(_) | FeatureExpr::Not(_) | FeatureExpr::True | FeatureExpr::False
    ) {
        description
    } else {
        format!("({description})")
    }
}

#[derive(Debug, Clone)]
pub struct RequiredEnum {
    pub entry: EnumEntry,
    pub extension_number: Option<u32>,
    pub condition: FeatureExpr,
}

#[derive(Debug)]
pub struct Selection {
    pub modules: Vec<SelectedModule>,
}

#[derive(Debug, Clone)]
pub struct SelectedModule {
    pub source_name: String,
    pub rust_name: String,
    pub prerequisite: FeatureExpr,
    pub guards: IndexSet<String>,
    pub types: IndexMap<String, FeatureExpr>,
    pub commands: IndexMap<String, FeatureExpr>,
    pub enums: Vec<RequiredEnum>,
}

pub fn select_vulkan(vk: &Registry, video: &Registry) -> Selection {
    let mut known_features = IndexSet::new();
    for feature in &vk.features {
        if api_matches(Some(&feature.api))
            && let Some(rust_name) = core_module_for_feature(&feature.name)
        {
            known_features.insert(rust_name.to_string());
        }
    }
    for extension in &vk.extensions {
        if supported_vulkan(&extension.supported) {
            known_features.insert(module_name(&extension.name));
        }
    }
    for extension in &video.extensions {
        if supported_vulkan(&extension.supported) {
            known_features.insert(module_name(&extension.name));
        }
    }

    let mut modules = Vec::new();

    for feature in &vk.features {
        if !api_matches(Some(&feature.api)) {
            continue;
        }
        let prerequisite = dependency_expr(feature.depends.as_deref(), &known_features);
        if prerequisite.is_false() {
            continue;
        }
        let module_idx = core_module_for_feature(&feature.name)
            .map(|rust_name| ensure_module(&mut modules, &feature.name, rust_name));
        if feature.apitype.as_deref() != Some("internal")
            && let Some(module_idx) = module_idx
        {
            modules[module_idx].guards.insert(feature.name.clone());
        }
        for require in &feature.requires {
            if !api_matches(require.api.as_deref()) {
                continue;
            }
            if let Some(module_idx) = module_idx {
                let activation = FeatureExpr::feature(modules[module_idx].rust_name.clone());
                add_require(
                    &mut modules[module_idx],
                    require,
                    None,
                    &activation,
                    &known_features,
                );
            }
        }
    }

    for extension in &vk.extensions {
        if !supported_vulkan(&extension.supported) {
            continue;
        }
        let prerequisite =
            extension_requirement_expr(extension.depends.as_deref(), &known_features);
        if prerequisite.is_false() {
            continue;
        }
        let rust_name = module_name(&extension.name);
        let module_idx = ensure_module(&mut modules, &extension.name, &rust_name);
        modules[module_idx].prerequisite = prerequisite;
        modules[module_idx].guards.insert(extension.name.clone());
        let activation = FeatureExpr::feature(rust_name);
        for require in &extension.requires {
            if !api_matches(require.api.as_deref()) {
                continue;
            }
            add_require(
                &mut modules[module_idx],
                require,
                Some(extension.number),
                &activation,
                &known_features,
            );
        }
    }

    for extension in &video.extensions {
        if !supported_vulkan(&extension.supported) {
            continue;
        }
        let prerequisite = dependency_expr(extension.depends.as_deref(), &known_features);
        if prerequisite.is_false() {
            continue;
        }
        let rust_name = module_name(&extension.name);
        let module_idx = ensure_module(&mut modules, &extension.name, &rust_name);
        modules[module_idx].prerequisite = prerequisite;
        modules[module_idx].guards.insert(extension.name.clone());
        let activation = FeatureExpr::feature(rust_name);
        for require in &extension.requires {
            if !api_matches(require.api.as_deref()) {
                continue;
            }
            add_require(
                &mut modules[module_idx],
                require,
                Some(extension.number),
                &activation,
                &known_features,
            );
        }
    }

    Selection { modules }
}

pub fn feature_names(vk: &Registry, video: &Registry) -> Vec<String> {
    select_vulkan(vk, video)
        .modules
        .into_iter()
        .map(|module| module.rust_name)
        .collect()
}

pub fn api_matches(api: Option<&str>) -> bool {
    api.map(|api| {
        api.split(',')
            .any(|part| matches!(part.trim(), "vulkan" | "vulkanbase"))
    })
    .unwrap_or(true)
}

fn supported_vulkan(supported: &str) -> bool {
    supported
        .split(',')
        .any(|part| matches!(part.trim(), "vulkan" | "vulkanbase"))
}

pub fn module_name(source: &str) -> String {
    source.to_ascii_lowercase()
}

fn core_module_for_feature(name: &str) -> Option<&'static str> {
    match name {
        "VK_BASE_VERSION_1_0"
        | "VK_COMPUTE_VERSION_1_0"
        | "VK_GRAPHICS_VERSION_1_0"
        | "VK_VERSION_1_0" => Some("core_1_0"),
        "VK_BASE_VERSION_1_1"
        | "VK_COMPUTE_VERSION_1_1"
        | "VK_GRAPHICS_VERSION_1_1"
        | "VK_VERSION_1_1" => Some("core_1_1"),
        "VK_BASE_VERSION_1_2"
        | "VK_COMPUTE_VERSION_1_2"
        | "VK_GRAPHICS_VERSION_1_2"
        | "VK_VERSION_1_2" => Some("core_1_2"),
        "VK_BASE_VERSION_1_3"
        | "VK_COMPUTE_VERSION_1_3"
        | "VK_GRAPHICS_VERSION_1_3"
        | "VK_VERSION_1_3" => Some("core_1_3"),
        "VK_BASE_VERSION_1_4"
        | "VK_COMPUTE_VERSION_1_4"
        | "VK_GRAPHICS_VERSION_1_4"
        | "VK_VERSION_1_4" => Some("core_1_4"),
        _ => None,
    }
}

fn ensure_module(modules: &mut Vec<SelectedModule>, source_name: &str, rust_name: &str) -> usize {
    if let Some((idx, _)) = modules
        .iter()
        .enumerate()
        .find(|(_, module)| module.rust_name == rust_name)
    {
        return idx;
    }

    modules.push(SelectedModule {
        source_name: source_name.to_string(),
        rust_name: rust_name.to_string(),
        prerequisite: FeatureExpr::True,
        guards: IndexSet::new(),
        types: IndexMap::new(),
        commands: IndexMap::new(),
        enums: Vec::new(),
    });
    modules.len() - 1
}

fn add_require(
    module: &mut SelectedModule,
    require: &crate::registry::Require,
    extension_number: Option<u32>,
    activation: &FeatureExpr,
    known_features: &IndexSet<String>,
) {
    let condition = FeatureExpr::all([
        activation.clone(),
        dependency_expr(require.depends.as_deref(), known_features),
    ]);
    if condition.is_false() {
        return;
    }
    for name in &require.types {
        insert_condition(&mut module.types, name, condition.clone());
    }
    for name in &require.commands {
        insert_condition(&mut module.commands, name, condition.clone());
    }
    module
        .enums
        .extend(require.enums.iter().cloned().map(|entry| RequiredEnum {
            entry,
            extension_number,
            condition: condition.clone(),
        }));
}

pub fn insert_condition(
    map: &mut IndexMap<String, FeatureExpr>,
    name: &str,
    condition: FeatureExpr,
) -> bool {
    match map.get(name) {
        Some(existing) => {
            let merged = FeatureExpr::any([existing.clone(), condition]);
            if *existing == merged {
                false
            } else {
                map.insert(name.to_string(), merged);
                true
            }
        }
        None => {
            map.insert(name.to_string(), condition);
            true
        }
    }
}

pub fn dependency_expr(depends: Option<&str>, known_features: &IndexSet<String>) -> FeatureExpr {
    depends
        .map(|depends| Parser::new(depends, known_features).parse())
        .unwrap_or(FeatureExpr::True)
}

pub fn extension_requirement_expr(
    depends: Option<&str>,
    known_features: &IndexSet<String>,
) -> FeatureExpr {
    FeatureExpr::all([
        FeatureExpr::feature("core_1_0"),
        dependency_expr(depends, known_features),
    ])
}

struct Parser<'a> {
    input: &'a [u8],
    pos: usize,
    known_features: &'a IndexSet<String>,
}

impl<'a> Parser<'a> {
    fn new(input: &'a str, known_features: &'a IndexSet<String>) -> Self {
        Self {
            input: input.as_bytes(),
            pos: 0,
            known_features,
        }
    }

    fn parse(mut self) -> FeatureExpr {
        let value = self.expr();
        self.skip_ws();
        assert_eq!(
            self.pos,
            self.input.len(),
            "unexpected dependency token at byte {}",
            self.pos
        );
        value
    }

    fn expr(&mut self) -> FeatureExpr {
        let mut value = self.term();
        loop {
            self.skip_ws();
            match self.peek() {
                Some(b'+') => {
                    self.pos += 1;
                    value = FeatureExpr::all([value, self.term()]);
                }
                Some(b',') => {
                    self.pos += 1;
                    value = FeatureExpr::any([value, self.term()]);
                }
                _ => return value,
            }
        }
    }

    fn term(&mut self) -> FeatureExpr {
        self.skip_ws();
        match self.peek() {
            Some(b'(') => {
                self.pos += 1;
                let value = self.expr();
                self.skip_ws();
                assert_eq!(self.peek(), Some(b')'), "unclosed dependency group");
                self.pos += 1;
                value
            }
            Some(b'!') => {
                self.pos += 1;
                FeatureExpr::not(self.term())
            }
            _ => {
                let token = self.token().to_string();
                symbol_expr(&token, self.known_features)
            }
        }
    }

    fn token(&mut self) -> &str {
        self.skip_ws();
        let start = self.pos;
        while let Some(ch) = self.peek() {
            if ch.is_ascii_alphanumeric() || matches!(ch, b'_' | b':') {
                self.pos += 1;
            } else {
                break;
            }
        }
        assert!(self.pos > start, "expected dependency name at byte {start}");
        std::str::from_utf8(&self.input[start..self.pos]).unwrap()
    }

    fn skip_ws(&mut self) {
        while self.peek().is_some_and(|ch| ch.is_ascii_whitespace()) {
            self.pos += 1;
        }
    }

    fn peek(&self) -> Option<u8> {
        self.input.get(self.pos).copied()
    }
}

fn symbol_expr(symbol: &str, known_features: &IndexSet<String>) -> FeatureExpr {
    if symbol.is_empty() || symbol.contains("::") {
        return FeatureExpr::True;
    }
    if let Some(feature) = core_module_for_feature(symbol) {
        return FeatureExpr::feature(feature);
    }
    let feature = module_name(symbol);
    if known_features.contains(&feature) {
        FeatureExpr::feature(feature)
    } else {
        FeatureExpr::False
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn rejects_malformed_dependencies() {
        let known = IndexSet::from(["a".into(), "b".into()]);
        for input in ["", "A+", "(A", "A)", "A B", "A|B", "A,,B"] {
            assert!(
                std::panic::catch_unwind(|| dependency_expr(Some(input), &known)).is_err(),
                "accepted {input:?}"
            );
        }
    }

    #[test]
    fn parses_depends_left_to_right() {
        let known_features = IndexSet::from(["a".to_string(), "c".to_string()]);
        assert_eq!(
            dependency_expr(Some("A+B,C"), &known_features),
            FeatureExpr::feature("c")
        );
        assert_eq!(
            dependency_expr(Some("A+(B,C)+D"), &known_features),
            FeatureExpr::False
        );
        assert_eq!(
            dependency_expr(Some("A+VkFoo::bar"), &known_features),
            FeatureExpr::feature("a")
        );
    }

    #[test]
    fn renders_dependency_cfgs() {
        let known_features = IndexSet::from([
            "vk_ext_a".to_string(),
            "vk_ext_b".to_string(),
            "vk_ext_c".to_string(),
        ]);
        let expr = dependency_expr(Some("VK_EXT_a+(VK_EXT_b,VK_EXT_c)"), &known_features);
        assert_eq!(
            expr.cfg_expr(),
            "all(feature = \"vk_ext_a\", any(feature = \"vk_ext_b\", feature = \"vk_ext_c\"))"
        );
    }

    #[test]
    fn keeps_conditions_on_extension_require_blocks() {
        let manifest_dir = std::path::Path::new(env!("CARGO_MANIFEST_DIR"));
        let vk = Registry::load(manifest_dir.join("../spec/vk.xml")).unwrap();
        let video = Registry::load(manifest_dir.join("../spec/video.xml")).unwrap();
        let selection = select_vulkan(&vk, &video);
        let maintenance11 = selection
            .modules
            .iter()
            .find(|module| module.rust_name == "vk_khr_maintenance11")
            .unwrap();

        let condition = |name: &str| {
            &maintenance11
                .enums
                .iter()
                .find(|required| required.entry.name == name)
                .unwrap()
                .condition
        };
        assert_eq!(
            condition("VK_SHADER_CREATE_INDEPENDENT_SETS_BIT_KHR"),
            &FeatureExpr::all([
                FeatureExpr::feature("vk_khr_maintenance11"),
                FeatureExpr::feature("vk_ext_shader_object"),
            ])
        );
        assert_eq!(
            condition("VK_IMAGE_CREATE_2_ALIAS_SINGLE_LAYER_DESCRIPTOR_BIT_KHR"),
            &FeatureExpr::all([
                FeatureExpr::feature("vk_khr_maintenance11"),
                FeatureExpr::feature("vk_khr_extended_flags"),
            ])
        );
    }

    #[test]
    fn simplifies_implied_feature_conditions() {
        let core = FeatureExpr::feature("core_1_1");
        let extension = FeatureExpr::feature("vk_ext_example");
        assert_eq!(
            FeatureExpr::any([
                core.clone(),
                FeatureExpr::all([core.clone(), extension.clone()]),
            ]),
            core
        );
        assert_eq!(
            FeatureExpr::all([
                FeatureExpr::feature("core_1_0"),
                FeatureExpr::feature("core_1_4"),
            ]),
            FeatureExpr::feature("core_1_4")
        );
    }

    #[test]
    fn derives_module_names() {
        assert_eq!(module_name("VK_KHR_surface"), "vk_khr_surface");
        assert_eq!(
            module_name("vulkan_video_codec_h264std_decode"),
            "vulkan_video_codec_h264std_decode"
        );
    }
}