use zerodds_idl::ast::{
Annotation, AnnotationParams, ConstrTypeDecl, Definition, Identifier, ScopedName,
Specification, StructDcl, TypeDecl, UnionDcl,
};
use zerodds_idl::errors::Span;
const SYNTH: Span = Span { start: 0, end: 0 };
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum DefaultExt {
Final,
Appendable,
Mutable,
}
impl DefaultExt {
#[must_use]
pub const fn cfg_default() -> Self {
#[cfg(feature = "ext-default-final")]
{
Self::Final
}
#[cfg(all(feature = "ext-default-mutable", not(feature = "ext-default-final")))]
{
Self::Mutable
}
#[cfg(all(
not(feature = "ext-default-final"),
not(feature = "ext-default-mutable")
))]
{
Self::Appendable
}
}
pub fn parse(token: &str) -> Option<Self> {
Some(match token {
"final" => Self::Final,
"appendable" => Self::Appendable,
"mutable" => Self::Mutable,
_ => return None,
})
}
fn alias(self) -> &'static str {
match self {
Self::Final => "final",
Self::Appendable => "appendable",
Self::Mutable => "mutable",
}
}
}
pub fn apply_default_extensibility(spec: &mut Specification, ext: DefaultExt) -> usize {
let mut patched = 0;
for def in &mut spec.definitions {
patched += patch_definition(def, &|annos| {
if has_any(annos, &["final", "appendable", "mutable", "extensibility"]) {
false
} else {
annos.push(make_annotation(ext.alias()));
true
}
});
}
patched
}
pub fn apply_default_nested(spec: &mut Specification, nested: bool) -> usize {
if !nested {
return 0;
}
let mut patched = 0;
for def in &mut spec.definitions {
patched += patch_definition(def, &|annos| {
if has_any(annos, &["nested", "topic"]) {
false
} else {
annos.push(make_annotation("nested"));
true
}
});
}
patched
}
fn patch_definition(def: &mut Definition, patch: &dyn Fn(&mut Vec<Annotation>) -> bool) -> usize {
match def {
Definition::Module(m) => {
let mut n = 0;
for d in &mut m.definitions {
n += patch_definition(d, patch);
}
n
}
Definition::Type(TypeDecl::Constr(ctd)) => {
let annos = match ctd {
ConstrTypeDecl::Struct(StructDcl::Def(s)) => &mut s.annotations,
ConstrTypeDecl::Union(UnionDcl::Def(u)) => &mut u.annotations,
ConstrTypeDecl::Enum(e) => &mut e.annotations,
_ => return 0,
};
usize::from(patch(annos))
}
_ => 0,
}
}
fn has_any(annos: &[Annotation], names: &[&str]) -> bool {
annos.iter().any(|a| {
a.name
.parts
.last()
.is_some_and(|p| names.contains(&p.text.as_str()))
})
}
fn make_annotation(name: &str) -> Annotation {
Annotation {
name: ScopedName::single(Identifier::new(name, SYNTH)),
params: AnnotationParams::None,
span: SYNTH,
}
}
#[cfg(test)]
mod tests {
#![allow(clippy::expect_used, clippy::panic)]
use super::*;
use zerodds_idl::config::ParserConfig;
use zerodds_idl::parser::parse;
#[test]
fn cfg_default_matches_active_feature() {
#[cfg(all(
not(feature = "ext-default-final"),
not(feature = "ext-default-mutable")
))]
assert_eq!(DefaultExt::cfg_default(), DefaultExt::Appendable);
#[cfg(feature = "ext-default-final")]
assert_eq!(DefaultExt::cfg_default(), DefaultExt::Final);
#[cfg(all(feature = "ext-default-mutable", not(feature = "ext-default-final")))]
assert_eq!(DefaultExt::cfg_default(), DefaultExt::Mutable);
}
fn parse_spec(src: &str) -> Specification {
parse(src, &ParserConfig::default()).expect("parse")
}
fn struct_annos(spec: &Specification, name: &str) -> Vec<String> {
for def in &spec.definitions {
if let Definition::Type(TypeDecl::Constr(ConstrTypeDecl::Struct(StructDcl::Def(s)))) =
def
{
if s.name.text == name {
return s
.annotations
.iter()
.filter_map(|a| a.name.parts.last().map(|p| p.text.clone()))
.collect();
}
}
}
panic!("struct {name} not found");
}
#[test]
fn unannotated_struct_gets_default() {
let mut spec = parse_spec("struct Plain { long a; };");
let n = apply_default_extensibility(&mut spec, DefaultExt::Appendable);
assert_eq!(n, 1);
assert_eq!(struct_annos(&spec, "Plain"), vec!["appendable"]);
}
#[test]
fn already_annotated_struct_untouched() {
let mut spec = parse_spec("@final struct Fixed { long a; };");
let n = apply_default_extensibility(&mut spec, DefaultExt::Appendable);
assert_eq!(n, 0);
assert_eq!(struct_annos(&spec, "Fixed"), vec!["final"]);
}
#[test]
fn extensibility_annotation_form_also_counts() {
let mut spec = parse_spec("@extensibility(MUTABLE) struct M { long a; };");
let n = apply_default_extensibility(&mut spec, DefaultExt::Final);
assert_eq!(n, 0, "@extensibility(...) must be recognised as annotated");
}
#[test]
fn default_applies_inside_modules() {
let mut spec = parse_spec("module Outer { struct Inner { long a; }; };");
let n = apply_default_extensibility(&mut spec, DefaultExt::Final);
assert_eq!(n, 1, "module-nested struct must be patched");
}
#[test]
fn unions_and_enums_are_patched() {
let src = "enum Color { RED, GREEN }; \
union U switch (long) { case 1: long x; };";
let mut spec = parse_spec(src);
let n = apply_default_extensibility(&mut spec, DefaultExt::Appendable);
assert_eq!(n, 2, "enum + union both get the default");
}
#[test]
fn default_nested_false_is_noop() {
let mut spec = parse_spec("struct S { long a; };");
assert_eq!(apply_default_nested(&mut spec, false), 0);
}
#[test]
fn default_nested_true_marks_unannotated() {
let mut spec = parse_spec("struct S { long a; };");
let n = apply_default_nested(&mut spec, true);
assert_eq!(n, 1);
assert!(struct_annos(&spec, "S").contains(&"nested".to_string()));
}
#[test]
fn default_nested_skips_topic_annotated() {
let mut spec = parse_spec("@topic struct T { long a; };");
assert_eq!(apply_default_nested(&mut spec, true), 0);
}
#[test]
fn parse_rejects_unknown_extensibility() {
assert_eq!(
DefaultExt::parse("appendable"),
Some(DefaultExt::Appendable)
);
assert_eq!(DefaultExt::parse("garbage"), None);
}
}