use std::fs;
use std::path::Path;
const SCHEMA: &str = "../espforge-espgenerate-template/src/espf.schema.kdl";
fn main() {
drivers();
component_vocab();
element_attrs();
}
fn drivers() {
println!("cargo:rerun-if-changed=src/drivers");
let drivers_dir = Path::new("src/drivers");
let dest_path = Path::new("src/drivers/generated_registry.rs");
let mut modules = Vec::new();
let mut match_arms = Vec::new();
let mut tags = Vec::new();
if let Ok(entries) = fs::read_dir(drivers_dir) {
for entry in entries.flatten() {
let path = entry.path();
println!("cargo:rerun-if-changed={}", path.display());
if path.extension().map_or(false, |ext| ext == "rs") {
let stem = path.file_stem().unwrap().to_str().unwrap().to_string();
if stem != "mod" && stem != "generated_registry" {
modules.push(format!("#[path = \"{stem}.rs\"]\npub mod {stem};"));
match_arms.push(format!(" \"{stem}\" => {stem}::init(device),"));
tags.push(format!("\"{stem}\""));
}
}
}
}
let generated_code = format!(
"// AUTO-GENERATED BY BUILD.RS - DO NOT EDIT MANUALLY\n\n\
#[allow(dead_code)]\n\
{}\n\n\
/// The device tags this registry routes, emitted alongside `route_driver`\n\
/// so the `espforge espf` catalog can be drift-checked against it.\n\
pub const TAGS: &[&str] = &[{}];\n\
\n\
pub fn route_driver(tag_name: &str, device: &espf::context::ParsedNode) -> Result<(), String> {{\n\
match tag_name {{\n\
{}\n\
_ => Err(format!(\"No compile-time driver engine found for runtime tag <{{}}>\", tag_name)),\n\
}}\n\
}}",
modules.join("\n"),
tags.join(", "),
match_arms.join("\n")
);
write_if_changed(dest_path, &generated_code);
}
fn component_vocab() {
println!("cargo:rerun-if-changed={SCHEMA}");
let src = fs::read_to_string(SCHEMA).unwrap_or_else(|e| panic!("cannot read {SCHEMA}: {e}"));
let doc = kdl::KdlDocument::parse_v2(&src)
.unwrap_or_else(|e| panic!("{SCHEMA} is not valid KDL v2:\n{e}"));
let mut components: Vec<String> = Vec::new();
for node in doc.nodes() {
if node.name().value() != "element" {
continue;
}
let section = node.get("section").and_then(|value| value.as_string());
if section != Some("components") {
continue;
}
let tag = node
.entries()
.iter()
.find(|entry| entry.name().is_none())
.and_then(|entry| entry.value().as_string())
.unwrap_or_else(|| panic!("an `element` in {SCHEMA} has no tag name"));
let is_bus_component = node
.get("bus_component")
.and_then(|value| value.as_bool())
.unwrap_or(false);
if is_bus_component {
components.push(tag.to_string());
}
}
let tags: Vec<String> = components.iter().map(|tag| format!("\"{tag}\"")).collect();
let out = format!(
"// AUTO-GENERATED BY BUILD.RS FROM espf.schema.kdl - DO NOT EDIT MANUALLY\n\n\
/// The bus-component tags in schema order.\n\
///\n\
/// These are the `components` elements marked `bus_component=#true` in\n\
/// `espf.schema.kdl`, so the catalog and the schema cannot disagree.\n\
pub const TAGS: &[&str] = &[{}];\n",
tags.join(", ")
);
write_if_changed(Path::new("src/components/generated_vocab.rs"), &out);
}
fn element_attrs() {
println!("cargo:rerun-if-changed={SCHEMA}");
let src = fs::read_to_string(SCHEMA).unwrap_or_else(|e| panic!("cannot read {SCHEMA}: {e}"));
let doc = kdl::KdlDocument::parse_v2(&src)
.unwrap_or_else(|e| panic!("{SCHEMA} is not valid KDL v2:\n{e}"));
let mut out = String::from(
"// AUTO-GENERATED BY BUILD.RS FROM espf.schema.kdl - DO NOT EDIT MANUALLY\n\
\n\
//! One `Attrs` struct per element in `espf.schema.kdl`: every attribute\n\
//! coerced to its declared `kind`, with the span it was written at.\n\
//!\n\
//! Lowering consumes these; the coercion itself is in\n\
//! `super::attrs`.\n",
);
for node in doc.nodes() {
if node.name().value() != "element" {
continue;
}
out.push('\n');
out.push_str(&element_module(node));
}
write_if_changed(Path::new("src/components/generated_elements.rs"), &out);
}
fn element_module(node: &kdl::KdlNode) -> String {
let tag = first_positional(node)
.unwrap_or_else(|| panic!("an `element` in {SCHEMA} has no tag name"));
let mut enums: Vec<String> = Vec::new();
let mut fields: Vec<String> = Vec::new();
let mut inits: Vec<String> = Vec::new();
let mut borrowed = false;
for attr in children(node, "attr") {
let name =
first_positional(&attr).unwrap_or_else(|| panic!("an `attr` of `{tag}` has no name"));
let kind =
prop_string(&attr, "kind").unwrap_or_else(|| panic!("`{tag}.{name}` has no kind"));
let required = prop_bool(&attr, "required").unwrap_or(false);
let default = prop_string(&attr, "default");
let choices = child_strings(&attr, "choices");
let lit = format!("{name:?}");
let (ty, init) = match (kind.as_str(), required, default.as_deref()) {
("ident", _, _) => (
"Spanned<&'a str>".to_string(),
format!("ident(node, {lit})?"),
),
("str", true, _) => (
"Spanned<&'a str>".to_string(),
format!("text(node, {lit})?"),
),
("str", false, Some(d)) => (
"Spanned<&'a str>".to_string(),
format!("text_or(node, {lit}, {d:?})?"),
),
("str", false, None) => (
"Option<Spanned<&'a str>>".to_string(),
format!("opt_text(node, {lit})?"),
),
("freq", true, _) => (
"Spanned<&'a str>".to_string(),
format!("frequency(node, {lit})?"),
),
("freq", false, Some(d)) => (
"Spanned<&'a str>".to_string(),
format!("frequency_or(node, {lit}, {d:?})?"),
),
("freq", false, None) => (
"Option<Spanned<&'a str>>".to_string(),
format!("opt_frequency(node, {lit})?"),
),
("bool", true, _) => ("Spanned<bool>".to_string(), format!("flag(node, {lit})?")),
("bool", false, Some(d)) => {
let d = match d {
"true" => "true",
"false" => "false",
other => panic!("`{tag}.{name}` has non-boolean default `{other}`"),
};
(
"Spanned<bool>".to_string(),
format!("flag_or(node, {lit}, {d})?"),
)
}
("u32", true, _) => match prop_string(&attr, "range") {
Some(range) => {
let (low, high) = parse_range(&range, &tag, &name);
(
"Spanned<u32>".to_string(),
format!("number_in(node, {lit}, {low}, {high})?"),
)
}
None => ("Spanned<u32>".to_string(), format!("number(node, {lit})?")),
},
("u32", false, Some(d)) => {
let d: u32 = d
.parse()
.unwrap_or_else(|_| panic!("`{tag}.{name}` default `{d}` is not a u32"));
match prop_string(&attr, "range") {
Some(range) => {
let (low, high) = parse_range(&range, &tag, &name);
(
"Spanned<u32>".to_string(),
format!("number_in_or(node, {lit}, {low}, {high}, {d})?"),
)
}
None => (
"Spanned<u32>".to_string(),
format!("number_or(node, {lit}, {d})?"),
),
}
}
("pin", true, _) => ("Spanned<u8>".to_string(), format!("pin(node, {lit})?")),
("pin", false, None) => (
"Option<Spanned<u8>>".to_string(),
format!("opt_pin(node, {lit})?"),
),
("hex", true, _) => ("Spanned<u8>".to_string(), format!("hex(node, {lit})?")),
("hex", false, None) => (
"Option<Spanned<u8>>".to_string(),
format!("opt_hex(node, {lit})?"),
),
("pin_or_ref", true, _) => (
"PinOrRef<'a>".to_string(),
format!("pin_or_ref(node, {lit})?"),
),
("ref", true, _) => ("Ref<'a>".to_string(), format!("reference(node, {lit})?")),
("ref", false, None) => (
"Option<Ref<'a>>".to_string(),
format!("opt_reference(node, {lit})?"),
),
("list", _, _) => (
"Vec<Spanned<&'a str>>".to_string(),
format!("list(node, {lit})"),
),
("choice", _, _) => {
if choices.is_empty() {
panic!("choice attribute `{tag}.{name}` has no `choices`");
}
let enum_name = camel_case(&field_name(&name));
enums.push(choice_enum(&enum_name, &name, &choices));
match (required, default.as_deref()) {
(true, _) => (
format!("Spanned<{enum_name}>"),
format!("parsed(node, {lit}, {enum_name}::parse)?"),
),
(false, Some(d)) => (
format!("Spanned<{enum_name}>"),
format!(
"parsed_or(node, {lit}, {enum_name}::parse, {enum_name}::{})?",
variant_name(d)
),
),
(false, None) => (
format!("Option<Spanned<{enum_name}>>"),
format!("opt_parsed(node, {lit}, {enum_name}::parse)?"),
),
}
}
(other, required, default) => panic!(
"unsupported attribute on `{tag}.{name}`: kind `{other}`, required {required}, default {default:?}"
),
};
let presence = match (required, default.as_deref()) {
(true, _) => "required".to_string(),
(false, Some(d)) => format!("optional, default {d:?}"),
(false, None) => "optional".to_string(),
};
let field = field_name(&name);
borrowed |= ty.contains("'a");
fields.push(format!(
" /// `{name}` (`{kind}`, {presence})\n pub {field}: {ty},"
));
inits.push(format!(" {field}: {init},"));
}
let (struct_head, impl_head, fn_head) = if borrowed {
(
"pub struct Attrs<'a> {",
"impl<'a> Attrs<'a> {",
"pub fn from_node(node: &'a ParsedNode<'a>) -> Result<Self, Issue<'a>> {",
)
} else {
(
"pub struct Attrs {",
"impl Attrs {",
"pub fn from_node<'n>(node: &'n ParsedNode<'n>) -> Result<Self, Issue<'n>> {",
)
};
let mut body = String::new();
for definition in &enums {
body.push_str(definition);
}
body.push_str(&format!(
concat!(
"/// `<{tag}>` from `espf.schema.kdl`.\n",
"#[derive(Debug, Clone)]\n",
"{struct_head}\n",
"{fields}\n",
"}}\n",
"\n",
"{impl_head}\n",
" /// Reads the attributes `espf.schema.kdl` declares for `<{tag}>`.\n",
" {fn_head}\n",
" Ok(Self {{\n",
"{inits}\n",
" }})\n",
" }}\n",
"}}\n",
),
tag = tag,
struct_head = struct_head,
impl_head = impl_head,
fn_head = fn_head,
fields = fields.join("\n"),
inits = inits.join("\n"),
));
format!(
concat!(
"pub mod {tag} {{\n",
" use espf::context::ParsedNode;\n",
"\n",
" use crate::components::attrs::*;\n",
"\n",
"{body}",
"}}\n",
),
tag = tag,
body = indent(&body, 4),
)
}
fn choice_enum(enum_name: &str, attr_name: &str, choices: &[String]) -> String {
let variants: Vec<String> = choices.iter().map(|c| variant_name(c)).collect();
let mut decls = String::new();
let mut parses = String::new();
let mut spellings = String::new();
for (choice, variant) in choices.iter().zip(&variants) {
decls.push_str(&format!(" /// `{choice}`\n {variant},\n"));
parses.push_str(&format!(
" {choice:?} => Some(Self::{variant}),\n"
));
spellings.push_str(&format!(" Self::{variant} => {choice:?},\n"));
}
let quoted: Vec<String> = choices.iter().map(|c| format!("{c:?}")).collect();
format!(
concat!(
"/// The `{attr_name}` values `espf.schema.kdl` allows.\n",
"#[derive(Debug, Clone, Copy, PartialEq, Eq)]\n",
"pub enum {enum_name} {{\n",
"{decls}",
"}}\n",
"\n",
"impl {enum_name} {{\n",
" /// The accepted spellings, in schema order.\n",
" pub const CHOICES: &'static [&'static str] = &[{choices}];\n",
"\n",
" /// Parses one of `CHOICES`; `None` for anything else.\n",
" pub fn parse(raw: &str) -> Option<Self> {{\n",
" match raw {{\n",
"{parses}",
" _ => None,\n",
" }}\n",
" }}\n",
"\n",
" /// The ESPF spelling of this value.\n",
" pub fn as_str(self) -> &'static str {{\n",
" match self {{\n",
"{spellings}",
" }}\n",
" }}\n",
"}}\n",
"\n",
),
attr_name = attr_name,
enum_name = enum_name,
decls = decls,
parses = parses,
spellings = spellings,
choices = quoted.join(", "),
)
}
fn parse_range(range: &str, tag: &str, name: &str) -> (u32, u32) {
let (low, high) = range
.split_once("..=")
.unwrap_or_else(|| panic!("`{tag}.{name}` range `{range}` is not `low..=high`"));
let parse = |bound: &str| {
bound
.parse::<u32>()
.unwrap_or_else(|_| panic!("`{tag}.{name}` range `{range}` is not `low..=high`"))
};
(parse(low), parse(high))
}
fn camel_case(text: &str) -> String {
let mut out = String::new();
for part in text.split(|c: char| !c.is_ascii_alphanumeric()) {
let mut chars = part.chars();
if let Some(first) = chars.next() {
out.extend(first.to_uppercase());
out.push_str(&chars.as_str().to_lowercase());
}
}
out
}
fn variant_name(choice: &str) -> String {
let name = camel_case(choice);
if name.starts_with(|c: char| c.is_ascii_digit()) {
format!("N{name}")
} else {
name
}
}
fn field_name(attr: &str) -> String {
let name: String = attr
.chars()
.map(|c| if c.is_ascii_alphanumeric() { c } else { '_' })
.collect();
if is_keyword(&name) {
format!("{name}_")
} else {
name
}
}
fn is_keyword(name: &str) -> bool {
matches!(
name,
"as" | "break"
| "const"
| "continue"
| "crate"
| "dyn"
| "else"
| "enum"
| "extern"
| "false"
| "fn"
| "for"
| "if"
| "impl"
| "in"
| "let"
| "loop"
| "match"
| "mod"
| "move"
| "mut"
| "pub"
| "ref"
| "return"
| "self"
| "static"
| "struct"
| "super"
| "trait"
| "true"
| "type"
| "unsafe"
| "use"
| "where"
| "while"
| "async"
| "await"
| "try"
| "union"
)
}
fn indent(text: &str, spaces: usize) -> String {
let pad = " ".repeat(spaces);
text.lines()
.map(|line| {
if line.is_empty() {
String::from("\n")
} else {
format!("{pad}{line}\n")
}
})
.collect()
}
fn first_positional(node: &kdl::KdlNode) -> Option<String> {
node.entries()
.iter()
.find(|entry| entry.name().is_none())
.and_then(|entry| entry.value().as_string())
.map(|value| value.to_string())
}
fn prop_string(node: &kdl::KdlNode, key: &str) -> Option<String> {
node.entries()
.iter()
.find(|entry| entry.name().is_some_and(|name| name.value() == key))
.and_then(|entry| entry.value().as_string())
.map(|value| value.to_string())
}
fn prop_bool(node: &kdl::KdlNode, key: &str) -> Option<bool> {
node.entries()
.iter()
.find(|entry| entry.name().is_some_and(|name| name.value() == key))
.and_then(|entry| entry.value().as_bool())
}
fn children<'a>(node: &'a kdl::KdlNode, name: &str) -> Vec<kdl::KdlNode> {
node.children()
.map(|doc| doc.nodes())
.unwrap_or(&[])
.iter()
.filter(|child| child.name().value() == name)
.cloned()
.collect()
}
fn child_strings(node: &kdl::KdlNode, name: &str) -> Vec<String> {
children(node, name)
.iter()
.flat_map(|child| {
child
.entries()
.iter()
.filter(|entry| entry.name().is_none())
})
.filter_map(|entry| entry.value().as_string().map(|value| value.to_string()))
.collect()
}
fn write_if_changed(path: &Path, contents: &str) {
let current = fs::read_to_string(path).unwrap_or_default();
if current != contents {
fs::write(path, contents)
.unwrap_or_else(|e| panic!("cannot write {}: {e}", path.display()));
}
}