use zerodds_ast::{Ast, TypeKind, TypeNode, VerbatimPlacement};
use zerodds_ast_core::Id;
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct Slots {
pub begin_file: Vec<String>,
pub before: Vec<String>,
pub begin: Vec<String>,
pub end: Vec<String>,
pub after: Vec<String>,
pub end_file: Vec<String>,
}
impl Slots {
#[must_use]
pub fn is_empty(&self) -> bool {
self.begin_file.is_empty()
&& self.before.is_empty()
&& self.begin.is_empty()
&& self.end.is_empty()
&& self.after.is_empty()
&& self.end_file.is_empty()
}
fn push(&mut self, placement: VerbatimPlacement, text: &str) {
let slot = match placement {
VerbatimPlacement::BeginFile => &mut self.begin_file,
VerbatimPlacement::BeforeDeclaration => &mut self.before,
VerbatimPlacement::BeginDeclaration => &mut self.begin,
VerbatimPlacement::EndDeclaration => &mut self.end,
VerbatimPlacement::AfterDeclaration => &mut self.after,
VerbatimPlacement::EndFile => &mut self.end_file,
};
slot.push(text.to_string());
}
}
#[must_use]
pub fn language_matches(language: &str, aliases: &[&str]) -> bool {
if language == "*" {
return true;
}
aliases
.iter()
.any(|alias| alias.eq_ignore_ascii_case(language))
}
#[must_use]
pub fn declaration_slots(ast: &Ast, id: Id<TypeNode>, aliases: &[&str]) -> Slots {
let mut slots = Slots::default();
let Ok(node) = ast.type_(id) else {
return slots;
};
gather(&node.facets, aliases, &mut slots, false);
for member in node.kind.members() {
if let Ok(member_node) = ast.member(*member) {
gather(&member_node.facets, aliases, &mut slots, true);
}
}
if let TypeKind::Union {
cases,
default_case,
..
} = &node.kind
{
for case in cases.iter().chain(default_case.iter()) {
let Ok(case_node) = ast.case(*case) else {
continue;
};
gather(&case_node.facets, aliases, &mut slots, true);
if let Ok(member_node) = ast.member(case_node.member) {
gather(&member_node.facets, aliases, &mut slots, true);
}
}
}
slots
}
#[must_use]
pub fn file_slots(ast: &Ast, aliases: &[&str]) -> (Vec<String>, Vec<String>) {
let mut begin = Vec::new();
let mut end = Vec::new();
let mut take = |facets: &zerodds_ast::Facets| {
let Some(xtypes) = facets.xtypes.as_ref() else {
return;
};
for directive in &xtypes.verbatim {
if !language_matches(&directive.language, aliases) {
continue;
}
match directive.placement {
VerbatimPlacement::BeginFile => begin.push(directive.text.clone()),
VerbatimPlacement::EndFile => end.push(directive.text.clone()),
_ => {}
}
}
};
for (_, node) in ast.modules() {
take(&node.facets);
}
for (_, node) in ast.types() {
take(&node.facets);
for member in node.kind.members() {
if let Ok(member_node) = ast.member(*member) {
take(&member_node.facets);
}
}
if let TypeKind::Union {
cases,
default_case,
..
} = &node.kind
{
for case in cases.iter().chain(default_case.iter()) {
let Ok(case_node) = ast.case(*case) else {
continue;
};
take(&case_node.facets);
if let Ok(member_node) = ast.member(case_node.member) {
take(&member_node.facets);
}
}
}
}
(begin, end)
}
pub trait Carrier: Sized {
fn verbatim(text: String) -> Self;
fn take_into_body(
&mut self,
name: &str,
begin: &mut Vec<String>,
end: &mut Vec<String>,
) -> bool;
}
pub fn weave<D: Carrier>(slots: Slots, name: &str, start: usize, items: &mut Vec<D>) {
weave_with(slots, start, items, D::verbatim, |declared, begin, end| {
for item in declared {
if item.take_into_body(name, begin, end) {
return;
}
}
});
}
pub fn weave_with<D>(
slots: Slots,
start: usize,
items: &mut Vec<D>,
make: impl Fn(String) -> D,
into_body: impl FnOnce(&mut [D], &mut Vec<String>, &mut Vec<String>),
) {
if slots.before.is_empty()
&& slots.begin.is_empty()
&& slots.end.is_empty()
&& slots.after.is_empty()
{
return;
}
let mut declared = items.split_off(start.min(items.len()));
let mut begin = slots.begin;
let mut end = slots.end;
if !begin.is_empty() || !end.is_empty() {
into_body(&mut declared, &mut begin, &mut end);
}
items.extend(slots.before.into_iter().map(&make));
items.extend(begin.into_iter().map(&make));
items.append(&mut declared);
items.extend(end.into_iter().map(&make));
items.extend(slots.after.into_iter().map(&make));
}
pub fn weave_file<D: Carrier>(ast: &Ast, aliases: &[&str], items: Vec<D>) -> Vec<D> {
let (begin, end) = file_slots(ast, aliases);
if begin.is_empty() && end.is_empty() {
return items;
}
let mut out: Vec<D> = begin.into_iter().map(D::verbatim).collect();
out.extend(items);
out.extend(end.into_iter().map(D::verbatim));
out
}
fn gather(facets: &zerodds_ast::Facets, aliases: &[&str], slots: &mut Slots, hoist: bool) {
let Some(xtypes) = facets.xtypes.as_ref() else {
return;
};
for directive in &xtypes.verbatim {
if !language_matches(&directive.language, aliases) {
continue;
}
match directive.placement {
VerbatimPlacement::BeginFile | VerbatimPlacement::EndFile if hoist => {}
_ if hoist => slots.begin.push(directive.text.clone()),
placement => slots.push(placement, &directive.text),
}
}
}
#[must_use]
pub fn aliases_of(language: zerodds_lang_cst::Language) -> &'static [&'static str] {
use zerodds_lang_cst::Language;
match language {
Language::C => aliases::C,
Language::Cpp => aliases::CPP,
Language::Java => aliases::JAVA,
Language::CSharp => aliases::CSHARP,
Language::Ada => aliases::ADA,
Language::Ada83 => aliases::ADA83,
Language::D => aliases::D,
Language::Elixir => aliases::ELIXIR,
Language::FSharp => aliases::FSHARP,
Language::Go => aliases::GO,
Language::Julia => aliases::JULIA,
Language::Kotlin => aliases::KOTLIN,
Language::Lua => aliases::LUA,
Language::Nim => aliases::NIM,
Language::Node => aliases::NODE,
Language::OCaml => aliases::OCAML,
Language::Python => aliases::PYTHON,
Language::Rust => aliases::RUST,
Language::Swift => aliases::SWIFT,
Language::TypeScript => aliases::TYPESCRIPT,
Language::Zig => aliases::ZIG,
}
}
pub mod aliases {
pub const C: &[&str] = &["c"];
pub const CPP: &[&str] = &["c++", "cpp", "cxx"];
pub const JAVA: &[&str] = &["java"];
pub const CSHARP: &[&str] = &["c#", "csharp", "cs"];
pub const ADA: &[&str] = &["ada"];
pub const ADA83: &[&str] = &["ada83", "ada-83"];
pub const D: &[&str] = &["d", "dlang"];
pub const ELIXIR: &[&str] = &["elixir", "ex"];
pub const FSHARP: &[&str] = &["f#", "fsharp", "fs"];
pub const GO: &[&str] = &["go", "golang"];
pub const JULIA: &[&str] = &["julia", "jl"];
pub const KOTLIN: &[&str] = &["kotlin", "kt"];
pub const LUA: &[&str] = &["lua"];
pub const NIM: &[&str] = &["nim"];
pub const NODE: &[&str] = &["javascript", "js", "node"];
pub const OCAML: &[&str] = &["ocaml", "ml"];
pub const PYTHON: &[&str] = &["python", "py"];
pub const RUST: &[&str] = &["rust", "rs"];
pub const SWIFT: &[&str] = &["swift"];
pub const TYPESCRIPT: &[&str] = &["typescript", "ts"];
pub const ZIG: &[&str] = &["zig"];
}
#[cfg(test)]
mod tests {
#![allow(clippy::expect_used, clippy::panic, clippy::unwrap_used)]
use super::*;
#[test]
fn the_wildcard_concerns_every_backend_and_names_compare_caselessly() {
assert!(language_matches("*", aliases::C));
assert!(language_matches("C++", aliases::CPP));
assert!(language_matches("CPP", aliases::CPP));
assert!(language_matches("c#", aliases::CSHARP));
assert!(!language_matches("java", aliases::CPP));
assert!(!language_matches("rust", aliases::JAVA));
}
fn model(idl: &str) -> Ast {
let parsed = zerodds_idl_parse::parse(idl);
assert!(
parsed.diagnostics().is_empty(),
"parse: {:?}",
parsed.diagnostics()
);
let lowered = zerodds_idl_lower::lower(&parsed.syntax(), "test.idl");
assert!(
lowered.diagnostics.is_empty(),
"lower: {:?}",
lowered.diagnostics
);
let mut ast = lowered.ast;
ast.resolve_all_by_name().expect("resolve");
ast
}
fn type_id(ast: &Ast, name: &str) -> Id<TypeNode> {
ast.types()
.find(|(_, node)| node.name == name)
.map(|(id, _)| id)
.expect("the type is in the model")
}
#[test]
fn every_placement_lands_in_its_own_slot() {
let ast = model(
"@verbatim(language = \"c\", placement = BEGIN_FILE, text = \"#include <a.h>\")
@verbatim(language = \"c\", placement = BEFORE_DECLARATION, text = \"/* b */\")
@verbatim(language = \"c\", placement = BEGIN_DECLARATION, text = \"/* c */\")
@verbatim(language = \"c\", placement = END_DECLARATION, text = \"/* d */\")
@verbatim(language = \"c\", placement = AFTER_DECLARATION, text = \"/* e */\")
@verbatim(language = \"c\", placement = END_FILE, text = \"/* f */\")
struct S { long x; };",
);
let slots = declaration_slots(&ast, type_id(&ast, "S"), aliases::C);
assert_eq!(slots.before, vec!["/* b */"]);
assert_eq!(slots.begin, vec!["/* c */"]);
assert_eq!(slots.end, vec!["/* d */"]);
assert_eq!(slots.after, vec!["/* e */"]);
assert_eq!(slots.begin_file, vec!["#include <a.h>"]);
assert_eq!(slots.end_file, vec!["/* f */"]);
let (begin, end) = file_slots(&ast, aliases::C);
assert_eq!(begin, vec!["#include <a.h>"]);
assert_eq!(end, vec!["/* f */"]);
}
#[test]
fn a_directive_for_another_language_is_left_to_that_backend() {
let ast = model(
"@verbatim(language = \"java\", text = \"// java only\")
@verbatim(text = \"// everyone\")
struct S { long x; };",
);
let slots = declaration_slots(&ast, type_id(&ast, "S"), aliases::C);
assert_eq!(slots.before, vec!["// everyone"]);
let slots = declaration_slots(&ast, type_id(&ast, "S"), aliases::JAVA);
assert_eq!(slots.before, vec!["// java only", "// everyone"]);
}
#[test]
fn a_member_directive_is_hoisted_into_the_aggregate_and_taken_once() {
let ast = model(
"struct S {
@verbatim(language = \"c++\", placement = BEFORE_DECLARATION, text = \"// m\")
long x;
};",
);
let slots = declaration_slots(&ast, type_id(&ast, "S"), aliases::CPP);
assert_eq!(slots.begin, vec!["// m"]);
assert!(slots.before.is_empty());
let (begin, end) = file_slots(&ast, aliases::CPP);
assert!(begin.is_empty() && end.is_empty());
}
}