use crate::common::{BodyText, Doc, Ident, Literal, Path};
use alloc::boxed::Box;
use alloc::string::{String, ToString};
use alloc::vec::Vec;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct File {
pub doc: Doc,
pub withs: Vec<Path>,
pub use_types: Vec<Path>,
pub package: Ident,
pub decls: Vec<Decl>,
}
impl Default for File {
fn default() -> Self {
Self {
doc: Doc::none(),
withs: Vec::new(),
use_types: Vec::new(),
package: Ident::new("Zd_Types"),
decls: Vec::new(),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Decl {
Type(TypeDecl),
Subtype(SubtypeDecl),
SubprogramBody(SubprogramBody),
Object(ObjectDecl),
EnumRep(EnumRep),
Subprogram(Subprogram),
Comment(Doc),
Verbatim(String),
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct TypeDecl {
pub doc: Doc,
pub name: Ident,
pub def: TypeDef,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum TypeDef {
Record(Record),
Enumeration(Vec<Ident>),
Derived(Path),
Array {
index: Vec<Index>,
element: Path,
},
Access(Path),
Interface,
Incomplete,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Record {
pub discriminants: Vec<Discriminant>,
pub components: Vec<Component>,
pub variant: Option<Variant>,
pub body_text: BodyText,
}
impl Record {
#[must_use]
pub fn plain(components: Vec<Component>) -> Self {
Self {
discriminants: Vec::new(),
components,
variant: None,
body_text: BodyText::none(),
}
}
#[must_use]
pub fn is_null(&self) -> bool {
self.components.is_empty()
&& self.variant.is_none()
&& self.body_text.begin.is_empty()
&& self.body_text.end.is_empty()
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Discriminant {
pub name: Ident,
pub type_: Path,
pub default: Option<Expr>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Component {
pub doc: Doc,
pub name: Ident,
pub type_: Path,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Variant {
pub discriminant: Ident,
pub arms: Vec<Arm>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Arm {
pub choices: Vec<Choice>,
pub components: Vec<Component>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Choice {
Value(Expr),
Others,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Index {
Range {
low: Expr,
high: Expr,
},
Unconstrained(Path),
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SubtypeDecl {
pub doc: Doc,
pub name: Ident,
pub mark: Path,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ObjectDecl {
pub doc: Doc,
pub name: Ident,
pub type_: Path,
pub value: Expr,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct EnumRep {
pub doc: Doc,
pub type_: Ident,
pub values: Vec<(Ident, Expr)>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Subprogram {
pub doc: Doc,
pub kind: SubprogramKind,
pub name: Ident,
pub params: Vec<Param>,
pub result: Option<Path>,
pub is_abstract: bool,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SubprogramKind {
Procedure,
Function,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Param {
pub name: Ident,
pub mode: Mode,
pub type_: Path,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Mode {
In,
Out,
InOut,
}
impl Mode {
pub const ALL: [Self; 3] = [Self::In, Self::Out, Self::InOut];
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::In => "in",
Self::Out => "out",
Self::InOut => "in out",
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Expr {
Literal(Literal),
Name(Path),
Call {
name: Path,
args: Vec<Expr>,
},
Negate(Box<Expr>),
Binary {
op: &'static str,
lhs: Box<Expr>,
rhs: Box<Expr>,
},
Unary {
op: &'static str,
value: Box<Expr>,
},
Selected {
value: Box<Expr>,
name: Ident,
},
Indexed {
value: Box<Expr>,
index: Box<Expr>,
},
Deref(Box<Expr>),
Slice {
value: Box<Expr>,
low: Box<Expr>,
high: Box<Expr>,
},
Attribute {
value: Box<Expr>,
name: Ident,
args: Vec<Expr>,
},
Convert {
mark: Path,
value: Box<Expr>,
},
Aggregate(Vec<Association>),
BoxDefault,
New {
mark: Path,
init: AllocInit,
},
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Association {
pub choice: Ident,
pub value: Expr,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum AllocInit {
Range {
low: Box<Expr>,
high: Box<Expr>,
},
Qualified(Box<Expr>),
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Stmt {
Null,
Var(VarDecl),
Subtype {
name: Ident,
mark: Path,
},
Assign {
place: Expr,
value: Expr,
},
Call(Expr),
Return(Option<Expr>),
If {
condition: Expr,
then: Vec<Stmt>,
otherwise: Option<Vec<Stmt>>,
},
For {
counter: Ident,
low: Expr,
high: Expr,
body: Vec<Stmt>,
},
While {
condition: Expr,
body: Vec<Stmt>,
},
Exit,
Declare {
vars: Vec<VarDecl>,
body: Vec<Stmt>,
},
Comment(Doc),
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct VarDecl {
pub name: Ident,
pub mark: Path,
pub constraint: Option<Expr>,
pub value: Option<Expr>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SubprogramBody {
pub spec: Subprogram,
pub body: Vec<Stmt>,
}
impl Expr {
#[must_use]
pub fn int(value: i128) -> Self {
Self::Literal(Literal::int(value))
}
#[must_use]
pub fn name(text: &str) -> Self {
Self::Name(Path::single(text))
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Primitive {
Boolean,
Integer8,
Integer16,
Integer32,
Integer64,
Unsigned8,
Unsigned16,
Unsigned32,
Unsigned64,
Float32,
Float64,
UnboundedString,
Address,
}
impl Primitive {
pub const ALL: [Self; 13] = [
Self::Boolean,
Self::Integer8,
Self::Integer16,
Self::Integer32,
Self::Integer64,
Self::Unsigned8,
Self::Unsigned16,
Self::Unsigned32,
Self::Unsigned64,
Self::Float32,
Self::Float64,
Self::UnboundedString,
Self::Address,
];
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::Boolean => "Boolean",
Self::Integer8 => "Interfaces.Integer_8",
Self::Integer16 => "Interfaces.Integer_16",
Self::Integer32 => "Interfaces.Integer_32",
Self::Integer64 => "Interfaces.Integer_64",
Self::Unsigned8 => "Interfaces.Unsigned_8",
Self::Unsigned16 => "Interfaces.Unsigned_16",
Self::Unsigned32 => "Interfaces.Unsigned_32",
Self::Unsigned64 => "Interfaces.Unsigned_64",
Self::Float32 => "Interfaces.IEEE_Float_32",
Self::Float64 => "Interfaces.IEEE_Float_64",
Self::UnboundedString => "Ada.Strings.Unbounded.Unbounded_String",
Self::Address => "System.Address",
}
}
#[must_use]
pub fn path(self) -> Path {
Path::new(&self.as_str().split('.').collect::<Vec<&str>>())
}
#[must_use]
pub fn unit(self) -> Option<Path> {
match self {
Self::Boolean => None,
Self::UnboundedString => Some(Path::new(&["Ada", "Strings", "Unbounded"])),
Self::Address => Some(Path::single("System")),
_ => Some(Path::single("Interfaces")),
}
}
}
pub const ESCAPE_MARKER: &str = "Idl";
pub const ESCAPE_SUFFIX: &str = "_Idl";
#[must_use]
pub fn legal_name(name: &str) -> String {
let mut out = String::with_capacity(name.len() + ESCAPE_MARKER.len());
if name.starts_with('_') {
out.push_str(ESCAPE_MARKER);
}
for ch in name.chars() {
if ch == '_' && out.ends_with('_') {
out.push_str(ESCAPE_MARKER);
}
out.push(ch);
}
if out.ends_with('_') {
out.push_str(ESCAPE_MARKER);
}
out
}
#[must_use]
pub fn escape_name(name: &str) -> String {
if is_reserved(name) {
let mut out = name.to_string();
out.push_str(ESCAPE_SUFFIX);
return out;
}
name.to_string()
}
#[must_use]
pub fn is_reserved(name: &str) -> bool {
RESERVED
.iter()
.any(|reserved| reserved.eq_ignore_ascii_case(name))
}
#[must_use]
pub fn ada_case(name: &str) -> String {
let mut out = String::with_capacity(name.len());
let mut at_word_start = true;
for ch in name.chars() {
if at_word_start {
for upper in ch.to_uppercase() {
out.push(upper);
}
} else {
out.push(ch);
}
at_word_start = ch == '_';
}
out
}
#[must_use]
pub fn fold(name: &str) -> String {
name.to_lowercase()
}
pub const RESERVED: &[&str] = &[
"abort",
"abs",
"abstract",
"accept",
"access",
"aliased",
"all",
"and",
"array",
"at",
"begin",
"body",
"case",
"constant",
"declare",
"delay",
"delta",
"digits",
"do",
"else",
"elsif",
"end",
"entry",
"exception",
"exit",
"for",
"function",
"generic",
"goto",
"if",
"in",
"interface",
"is",
"limited",
"loop",
"mod",
"new",
"not",
"null",
"of",
"or",
"others",
"out",
"overriding",
"package",
"pragma",
"private",
"procedure",
"protected",
"raise",
"range",
"record",
"rem",
"renames",
"requeue",
"return",
"reverse",
"select",
"separate",
"some",
"subtype",
"synchronized",
"tagged",
"task",
"terminate",
"then",
"type",
"until",
"use",
"when",
"while",
"with",
"xor",
"ada",
"interfaces",
"strings",
"system",
"standard",
"unbounded",
"boolean",
"character",
"duration",
"float",
"integer",
"natural",
"positive",
"string",
"wide_character",
"wide_string",
"zd_types",
];
#[cfg(test)]
mod tests {
#![allow(clippy::expect_used, clippy::panic, clippy::unwrap_used)]
use super::*;
#[test]
fn every_named_type_writes_its_own_spelling() {
let mut seen = Vec::new();
for primitive in Primitive::ALL {
let text = primitive.as_str();
assert!(!seen.contains(&text), "{text} is used twice");
seen.push(text);
}
assert_eq!(seen.len(), Primitive::ALL.len());
}
#[test]
fn no_named_type_has_an_implementation_defined_width() {
for primitive in Primitive::ALL {
assert!(
!matches!(
primitive.as_str(),
"Integer" | "Float" | "Long_Float" | "Long_Long_Float" | "Natural"
),
"{} has no fixed width",
primitive.as_str()
);
}
}
#[test]
fn a_fixed_width_type_names_the_interfaces_package() {
for primitive in [
Primitive::Integer8,
Primitive::Integer16,
Primitive::Integer32,
Primitive::Integer64,
Primitive::Unsigned8,
Primitive::Unsigned16,
Primitive::Unsigned32,
Primitive::Unsigned64,
Primitive::Float32,
Primitive::Float64,
] {
assert!(
primitive.as_str().starts_with("Interfaces."),
"{} is not qualified",
primitive.as_str()
);
assert_eq!(primitive.unit(), Some(Path::single("Interfaces")));
}
}
#[test]
fn a_qualified_name_splits_into_its_segments() {
let path = Primitive::UnboundedString.path();
let segments: Vec<&str> = path.segments().iter().map(Ident::as_str).collect();
assert_eq!(
segments,
["Ada", "Strings", "Unbounded", "Unbounded_String"]
);
}
#[test]
fn a_reserved_word_escapes_with_a_letter_and_never_an_underscore() {
assert_eq!(escape_name("type"), "type_Idl");
assert_eq!(escape_name("range"), "range_Idl");
assert_eq!(escape_name("payload"), "payload");
for name in RESERVED {
let escaped = escape_name(name);
assert!(!escaped.ends_with('_'), "{escaped} ends in an underscore");
assert!(!escaped.contains("__"), "{escaped} has two in a row");
}
}
#[test]
fn the_reserved_check_folds_case_because_ada_does() {
for spelling in ["type", "Type", "TYPE", "TyPe"] {
assert!(is_reserved(spelling), "{spelling} was not recognised");
assert_eq!(escape_name(spelling), alloc::format!("{spelling}_Idl"));
}
}
#[test]
fn the_names_a_generated_spec_relies_on_escape_although_they_would_parse() {
for name in [
"Interfaces",
"Ada",
"System",
"Boolean",
"String",
"Integer",
] {
assert!(is_reserved(name), "{name} is not guarded");
}
}
#[test]
fn the_package_name_is_guarded_against_its_own_declarations() {
assert!(is_reserved("Zd_Types"));
}
#[test]
fn an_identifier_may_not_start_with_an_underscore() {
assert_eq!(legal_name("_long"), "Idl_long");
assert_eq!(legal_name("_d"), "Idl_d");
assert_eq!(legal_name("plain"), "plain");
}
#[test]
fn an_identifier_may_not_hold_two_underscores_in_a_row() {
assert_eq!(legal_name("a__b"), "a_Idl_b");
assert_eq!(legal_name("a___b"), "a_Idl_Idl_b");
assert_eq!(legal_name("a_b"), "a_b");
}
#[test]
fn an_identifier_may_not_end_in_an_underscore() {
assert_eq!(legal_name("foo_"), "foo_Idl");
assert_eq!(legal_name("_foo_"), "Idl_foo_Idl");
}
#[test]
fn every_repair_produces_something_ada_reads_as_an_identifier() {
for name in [
"_long", "_d", "a__b", "a___b", "foo_", "_foo_", "plain", "a_b", "_a__b_",
] {
let repaired = legal_name(name);
assert!(
repaired.starts_with(|ch: char| ch.is_alphabetic()),
"{repaired} does not start with a letter"
);
assert!(!repaired.ends_with('_'), "{repaired} ends in an underscore");
assert!(!repaired.contains("__"), "{repaired} has two in a row");
}
}
#[test]
fn ada_casing_moves_the_first_letter_of_each_word_and_nothing_else() {
assert_eq!(ada_case("value"), "Value");
assert_eq!(ada_case("my_field"), "My_Field");
assert_eq!(ada_case("IDLE"), "IDLE");
assert_eq!(ada_case("aBc"), "ABc");
assert_eq!(ada_case(""), "");
}
#[test]
fn two_spellings_that_differ_only_in_case_have_one_fold_key() {
assert_eq!(fold("Value"), fold("value"));
assert_eq!(fold("TYPE_Idl"), fold("type_idl"));
assert_ne!(fold("Value"), fold("Values"));
}
#[test]
fn the_reserved_list_has_no_duplicates_and_is_folded_already() {
let mut seen: Vec<String> = Vec::new();
for name in RESERVED {
let key = fold(name);
assert!(!seen.contains(&key), "{name} is listed twice");
seen.push(key);
}
}
#[test]
fn the_reserved_list_holds_every_ada_2012_reserved_word() {
for word in [
"abort",
"accept",
"aliased",
"elsif",
"overriding",
"requeue",
"some",
"synchronized",
"terminate",
"xor",
] {
assert!(is_reserved(word), "{word} is missing");
}
}
#[test]
fn a_record_with_nothing_in_it_is_a_null_record() {
assert!(Record::plain(Vec::new()).is_null());
assert!(
!Record::plain(alloc::vec![Component {
doc: Doc::none(),
name: Ident::new("X"),
type_: Primitive::Integer32.path(),
}])
.is_null()
);
}
#[test]
fn a_record_with_only_a_variant_part_is_not_a_null_record() {
let record = Record {
body_text: crate::common::BodyText::none(),
discriminants: alloc::vec![Discriminant {
name: Ident::new("D"),
type_: Primitive::Boolean.path(),
default: Some(Expr::name("False")),
}],
components: Vec::new(),
variant: Some(Variant {
discriminant: Ident::new("D"),
arms: alloc::vec![Arm {
choices: alloc::vec![Choice::Others],
components: Vec::new(),
}],
}),
};
assert!(!record.is_null());
}
#[test]
fn an_array_type_nests_outermost_dimension_first() {
let def = TypeDef::Array {
index: alloc::vec![Index::Range {
low: Expr::int(0),
high: Expr::int(1),
}],
element: Path::single("Zd_Arr_Long_3"),
};
let TypeDef::Array { index, element } = &def else {
panic!("not an array");
};
assert_eq!(index.len(), 1);
assert_eq!(element.last().map(Ident::as_str), Some("Zd_Arr_Long_3"));
}
#[test]
fn every_parameter_mode_writes_its_ada_spelling() {
assert_eq!(Mode::In.as_str(), "in");
assert_eq!(Mode::Out.as_str(), "out");
assert_eq!(Mode::InOut.as_str(), "in out");
assert_eq!(Mode::ALL.len(), 3);
}
}