use anyhow::Result;
use indexmap::IndexMap;
use super::SpecDocs;
use crate::ir::{CommandScope, RawCommand, RawParam};
const INSTANCE_HANDLES: &[&str] = &["VkInstance", "VkPhysicalDevice"];
const DEVICE_HANDLES: &[&str] = &["VkDevice", "VkQueue", "VkCommandBuffer"];
pub fn parse_commands(
docs: &SpecDocs<'_, '_>,
_spec_name: &str,
) -> Result<IndexMap<String, RawCommand>> {
let mut commands: IndexMap<String, RawCommand> = IndexMap::new();
for node in docs.section_children("commands") {
if node.tag_name().name() != "command" {
continue;
}
parse_command_node(node, &mut commands)?;
}
alias_fixup(&mut commands);
Ok(commands)
}
fn parse_command_node(
node: roxmltree::Node<'_, '_>,
commands: &mut IndexMap<String, RawCommand>,
) -> Result<()> {
let cmd_alias = node.attribute("alias").map(str::to_string).or_else(|| {
node.children()
.find(|n| n.is_element() && n.tag_name().name() == "alias")
.and_then(|n| n.attribute("name"))
.map(str::to_string)
});
let cmd_api = node.attribute("api").map(str::to_string);
if let Some(proto) = node
.children()
.find(|n| n.is_element() && n.tag_name().name() == "proto")
{
let (name, return_type) = parse_proto(proto);
let params = node
.children()
.filter(|n| n.is_element() && n.tag_name().name() == "param")
.filter(|n| {
n.attribute("api").is_none_or(|a| {
a.split(',')
.any(|s| s.trim() == "vulkan" || s.trim() == "vulkanbase")
})
})
.map(parse_param)
.collect::<Vec<_>>();
let cmd = RawCommand {
name: name.clone(),
api: cmd_api,
return_type,
params,
alias: cmd_alias,
};
commands.entry(name).or_insert(cmd);
} else {
let name = match node.attribute("name") {
Some(n) => n.to_string(),
None => return Ok(()), };
let cmd = RawCommand {
name: name.clone(),
api: cmd_api,
return_type: String::new(), params: Vec::new(),
alias: cmd_alias,
};
commands.entry(name).or_insert(cmd);
}
Ok(())
}
fn parse_proto(proto: roxmltree::Node<'_, '_>) -> (String, String) {
let mut return_type = String::new();
let mut name = String::new();
for child in proto.children() {
if child.is_text() {
return_type.push_str(child.text().unwrap_or(""));
} else if child.is_element() {
match child.tag_name().name() {
"name" => {
name = child.text().unwrap_or("").to_string();
}
_ => {
return_type.push_str(child.text().unwrap_or(""));
}
}
}
}
(name, return_type.trim().to_string())
}
fn parse_param(param: roxmltree::Node<'_, '_>) -> RawParam {
let api = param.attribute("api").map(str::to_string);
let param_name = param
.children()
.find(|n| n.is_element() && n.tag_name().name() == "name")
.and_then(|n| n.text())
.unwrap_or("")
.trim_start_matches('*')
.to_string();
let mut type_name = String::new();
for child in param.children() {
if child.is_element() && matches!(child.tag_name().name(), "ptype" | "type") {
let t = child.text().unwrap_or("");
type_name = t.replace("struct ", "").trim().to_string();
break;
}
}
let full_raw = super::extract_raw_c(param);
let full_trimmed = full_raw.trim();
let type_raw = if !param_name.is_empty() {
if let Some(name_pos) = full_trimmed.rfind(param_name.as_str()) {
let after = full_trimmed[name_pos + param_name.len()..].trim();
if after.is_empty() {
full_trimmed[..name_pos].trim().to_string()
} else {
full_trimmed.to_string()
}
} else {
full_trimmed.to_string()
}
} else {
full_trimmed.to_string()
};
if type_name.is_empty() {
type_name = extract_base_type(&type_raw);
}
RawParam {
name: param_name,
type_raw,
type_name,
api,
}
}
fn extract_base_type(raw: &str) -> String {
raw.replace("const", "")
.replace("unsigned", "")
.replace("struct", "")
.replace('*', "")
.split_whitespace()
.next()
.unwrap_or("")
.to_string()
}
fn alias_fixup(commands: &mut IndexMap<String, RawCommand>) {
let aliases_needing_fixup: Vec<String> = commands
.values()
.filter(|c| c.return_type.is_empty() && c.alias.is_some())
.map(|c| c.name.clone())
.collect();
for name in aliases_needing_fixup {
let resolved = walk_alias_chain(commands, &name);
if let Some((ret, params)) = resolved {
if let Some(cmd) = commands.get_mut(&name) {
cmd.return_type = ret;
cmd.params = params;
}
} else {
eprintln!(
"warning: could not resolve alias chain for command '{}'",
name
);
}
}
}
fn walk_alias_chain(
commands: &IndexMap<String, RawCommand>,
name: &str,
) -> Option<(String, Vec<RawParam>)> {
let mut current = name;
let mut visited = std::collections::HashSet::new();
loop {
if visited.contains(current) {
return None; }
visited.insert(current);
let cmd = commands.get(current)?;
if !cmd.return_type.is_empty() {
return Some((cmd.return_type.clone(), cmd.params.clone()));
}
current = cmd.alias.as_deref()?;
}
}
pub fn infer_vulkan_scope(cmd: &RawCommand) -> CommandScope {
infer_scope(cmd)
}
fn infer_scope(cmd: &RawCommand) -> CommandScope {
if cmd.name == "vkGetInstanceProcAddr" {
return CommandScope::Unknown;
}
let first_type = cmd
.params
.first()
.map(|p| p.type_name.as_str())
.unwrap_or("");
if DEVICE_HANDLES.contains(&first_type) {
CommandScope::Device
} else if INSTANCE_HANDLES.contains(&first_type) {
CommandScope::Instance
} else {
CommandScope::Global
}
}