use crate::common::{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 module: Path,
pub imports: Vec<Import>,
pub decls: Vec<Decl>,
}
impl Default for File {
fn default() -> Self {
Self {
doc: Doc::none(),
module: Path::new(&["zdtypes"]),
imports: Vec::new(),
decls: Vec::new(),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Import {
pub path: Path,
pub symbols: Vec<Ident>,
}
impl Import {
#[must_use]
pub fn selective(path: &[&str], symbol: &str) -> Self {
Self {
path: Path::new(path),
symbols: alloc::vec![Ident::new(symbol)],
}
}
#[must_use]
pub fn module(path: &[&str]) -> Self {
Self {
path: Path::new(path),
symbols: Vec::new(),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Decl {
Struct(StructDecl),
Enum(EnumDecl),
Alias(AliasDecl),
Const(ConstDecl),
Interface(InterfaceDecl),
Func(FuncDecl),
Comment(Doc),
Verbatim(String),
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct FuncDecl {
pub doc: Doc,
pub name: Ident,
pub params: Vec<Param>,
pub returns: Option<Type>,
pub body: Vec<Stmt>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Stmt {
Var {
name: Ident,
type_: Option<Type>,
value: Option<Expr>,
},
Assign {
place: Expr,
value: Expr,
},
Expr(Expr),
Return(Option<Expr>),
If {
condition: Expr,
then: Vec<Stmt>,
otherwise: Option<Vec<Stmt>>,
},
For {
counter: Ident,
type_: Box<Type>,
bound: Box<Expr>,
body: Vec<Stmt>,
},
While {
condition: Expr,
body: Vec<Stmt>,
},
Break,
Comment(Doc),
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct StructDecl {
pub doc: Doc,
pub name: Ident,
pub members: Vec<Member>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Member {
Field(Field),
Union(Vec<Field>),
Verbatim(String),
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Field {
pub doc: Doc,
pub udas: Vec<Uda>,
pub name: Ident,
pub type_: Type,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Uda {
pub text: String,
}
impl Uda {
#[must_use]
pub fn keyed(key: &str, value: &str) -> Self {
let mut text = key.to_string();
text.push(':');
text.push_str(value);
Self { text }
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct EnumDecl {
pub doc: Doc,
pub name: Ident,
pub base: Option<Type>,
pub members: Vec<EnumMember>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct EnumMember {
pub doc: Doc,
pub name: Ident,
pub value: Expr,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct AliasDecl {
pub doc: Doc,
pub name: Ident,
pub type_: Type,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ConstDecl {
pub doc: Doc,
pub name: Ident,
pub type_: Option<Type>,
pub value: Expr,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct InterfaceDecl {
pub doc: Doc,
pub name: Ident,
pub methods: Vec<Method>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Method {
pub doc: Doc,
pub name: Ident,
pub returns: Option<Type>,
pub params: Vec<Param>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Param {
pub name: Ident,
pub type_: Type,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Type {
Primitive(Primitive),
Named(Ident),
Slice(Box<Type>),
Array {
length: Box<Expr>,
element: Box<Type>,
},
Pointer(Box<Type>),
Nullable(Box<Type>),
}
impl Type {
#[must_use]
pub fn named(text: &str) -> Self {
Self::Named(Ident::new(text))
}
#[must_use]
pub fn slice(element: Self) -> Self {
Self::Slice(Box::new(element))
}
#[must_use]
pub fn pointer(target: Self) -> Self {
Self::Pointer(Box::new(target))
}
#[must_use]
pub fn nullable(target: Self) -> Self {
Self::Nullable(Box::new(target))
}
#[must_use]
pub fn array(length: i128, element: Self) -> Self {
Self::Array {
length: Box::new(Expr::Literal(Literal::int(length))),
element: Box::new(element),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Primitive {
Void,
Bool,
Byte,
UByte,
Short,
UShort,
Int,
UInt,
Long,
ULong,
Float,
Double,
String,
}
impl Primitive {
pub const ALL: [Self; 13] = [
Self::Void,
Self::Bool,
Self::Byte,
Self::UByte,
Self::Short,
Self::UShort,
Self::Int,
Self::UInt,
Self::Long,
Self::ULong,
Self::Float,
Self::Double,
Self::String,
];
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::Void => "void",
Self::Bool => "bool",
Self::Byte => "byte",
Self::UByte => "ubyte",
Self::Short => "short",
Self::UShort => "ushort",
Self::Int => "int",
Self::UInt => "uint",
Self::Long => "long",
Self::ULong => "ulong",
Self::Float => "float",
Self::Double => "double",
Self::String => "string",
}
}
#[must_use]
pub const fn bits(self) -> Option<u16> {
Some(match self {
Self::Void | Self::String => return None,
Self::Bool | Self::Byte | Self::UByte => 8,
Self::Short | Self::UShort => 16,
Self::Int | Self::UInt | Self::Float => 32,
Self::Long | Self::ULong | Self::Double => 64,
})
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Expr {
Literal(Literal),
Name(Ident),
Binary {
op: &'static str,
lhs: Box<Expr>,
rhs: Box<Expr>,
},
Cast {
type_: Box<Type>,
value: Box<Expr>,
},
Paren(Box<Expr>),
Call {
func: Box<Expr>,
args: Vec<Expr>,
},
Dot {
value: Box<Expr>,
name: Ident,
},
Index {
value: Box<Expr>,
index: Box<Expr>,
},
SliceTo {
value: Box<Expr>,
high: Box<Expr>,
},
Unary {
op: &'static str,
value: Box<Expr>,
},
StructLit {
name: Ident,
fields: Vec<(Option<Ident>, Expr)>,
},
ArrayLit(Vec<Expr>),
NewSlice {
element: Box<Type>,
length: Box<Expr>,
},
}
impl Expr {
#[must_use]
pub fn int(value: i128) -> Self {
Self::Literal(Literal::int(value))
}
#[must_use]
pub fn suffixed_int(value: i128, suffix: &str) -> Self {
let mut text = alloc::format!("{value}");
text.push_str(suffix);
Self::Literal(Literal::Int(text))
}
}
#[must_use]
pub fn escape_name(name: &str) -> String {
if RESERVED.contains(&name) {
let mut out = name.to_string();
out.push('_');
return out;
}
name.to_string()
}
pub const RESERVED: &[&str] = &[
"abstract",
"alias",
"align",
"asm",
"assert",
"auto",
"body",
"bool",
"break",
"byte",
"case",
"cast",
"catch",
"cdouble",
"cent",
"cfloat",
"char",
"class",
"const",
"continue",
"creal",
"dchar",
"debug",
"default",
"delegate",
"delete",
"deprecated",
"do",
"double",
"else",
"enum",
"export",
"extern",
"false",
"final",
"finally",
"float",
"for",
"foreach",
"foreach_reverse",
"function",
"goto",
"idouble",
"if",
"ifloat",
"immutable",
"import",
"in",
"inout",
"int",
"interface",
"invariant",
"ireal",
"is",
"lazy",
"long",
"macro",
"mixin",
"module",
"new",
"nothrow",
"null",
"out",
"override",
"package",
"pragma",
"private",
"protected",
"public",
"pure",
"real",
"ref",
"return",
"scope",
"shared",
"short",
"static",
"struct",
"super",
"switch",
"synchronized",
"template",
"this",
"throw",
"true",
"try",
"typeid",
"typeof",
"ubyte",
"ucent",
"uint",
"ulong",
"union",
"unittest",
"ushort",
"version",
"void",
"wchar",
"while",
"with",
"__FILE__",
"__FILE_FULL_PATH__",
"__FUNCTION__",
"__LINE__",
"__MODULE__",
"__PRETTY_FUNCTION__",
"__gshared",
"__parameters",
"__traits",
"__vector",
"Object",
"TypeInfo",
"dstring",
"ptrdiff_t",
"size_t",
"string",
"wstring",
"Nullable",
"std",
"alignof",
"init",
"mangleof",
"sizeof",
"stringof",
"tupleof",
];
#[cfg(test)]
mod tests {
#![allow(clippy::expect_used, clippy::panic, clippy::unwrap_used)]
use super::*;
#[test]
fn every_primitive_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_primitive_has_a_target_dependent_width() {
for primitive in Primitive::ALL {
assert!(
!matches!(
primitive.as_str(),
"real" | "cent" | "ucent" | "creal" | "ireal"
),
"{} has no fixed width on every target",
primitive.as_str()
);
}
}
#[test]
fn no_primitive_is_a_utf_code_unit() {
for primitive in Primitive::ALL {
assert!(
!matches!(primitive.as_str(), "char" | "wchar" | "dchar"),
"{} is a UTF code unit and not an IDL character",
primitive.as_str()
);
}
}
#[test]
fn a_d_int_is_thirty_two_bits_and_a_d_long_is_sixty_four() {
assert_eq!(Primitive::Int.bits(), Some(32));
assert_eq!(Primitive::Long.bits(), Some(64));
assert_eq!(Primitive::UInt.bits(), Some(32));
assert_eq!(Primitive::ULong.bits(), Some(64));
}
#[test]
fn only_void_and_string_have_no_width() {
for primitive in Primitive::ALL {
let has_width = primitive.bits().is_some();
assert_eq!(
has_width,
!matches!(primitive, Primitive::Void | Primitive::String),
"{} disagrees about having a width",
primitive.as_str()
);
}
}
#[test]
fn a_keyword_escapes_with_a_trailing_underscore() {
assert_eq!(escape_name("version"), "version_");
assert_eq!(escape_name("scope"), "scope_");
assert_eq!(escape_name("payload"), "payload");
}
#[test]
fn a_type_keyword_escapes_because_a_declaration_could_not_shadow_it() {
for name in ["int", "long", "real", "string", "union", "byte"] {
assert_eq!(escape_name(name), alloc::format!("{name}_"));
}
}
#[test]
fn a_name_the_generated_import_binds_escapes_too() {
assert_eq!(escape_name("Nullable"), "Nullable_");
assert_eq!(escape_name("std"), "std_");
}
#[test]
fn a_built_in_property_name_escapes() {
assert_eq!(escape_name("init"), "init_");
assert_eq!(escape_name("tupleof"), "tupleof_");
}
#[test]
fn the_reserved_list_has_no_duplicates() {
let mut seen: Vec<&str> = Vec::new();
for name in RESERVED {
assert!(!seen.contains(name), "{name} is listed twice");
seen.push(name);
}
}
#[test]
fn every_primitive_spelling_is_reserved_or_deliberately_not() {
for primitive in Primitive::ALL {
assert!(
RESERVED.contains(&primitive.as_str()),
"{} can be shadowed",
primitive.as_str()
);
}
}
#[test]
fn an_array_type_nests_outermost_dimension_first() {
let type_ = Type::array(2, Type::array(3, Type::Primitive(Primitive::Int)));
let Type::Array { length, element } = &type_ else {
panic!("not an array");
};
assert_eq!(**length, Expr::int(2));
let Type::Array { length, .. } = &**element else {
panic!("the element is not an array");
};
assert_eq!(**length, Expr::int(3));
}
#[test]
fn a_field_carries_its_idl_spelling_in_a_uda() {
let field = Field {
doc: Doc::none(),
udas: alloc::vec![Uda::keyed("idl", "version")],
name: Ident::new("version_"),
type_: Type::Primitive(Primitive::Int),
};
assert_eq!(field.udas.len(), 1);
assert_eq!(field.udas[0].text, "idl:version");
}
#[test]
fn a_struct_can_hold_an_anonymous_union_member() {
let decl = StructDecl {
doc: Doc::none(),
name: Ident::new("Payload"),
members: alloc::vec![
Member::Field(Field {
doc: Doc::none(),
udas: Vec::new(),
name: Ident::new("_d"),
type_: Type::Primitive(Primitive::Int),
}),
Member::Union(alloc::vec![Field {
doc: Doc::none(),
udas: Vec::new(),
name: Ident::new("idle"),
type_: Type::Primitive(Primitive::Int),
}]),
],
};
assert!(matches!(decl.members[1], Member::Union(_)));
}
#[test]
fn a_suffixed_literal_keeps_the_suffix() {
assert_eq!(
Expr::suffixed_int(1, "UL"),
Expr::Literal(Literal::Int("1UL".into()))
);
assert_eq!(Expr::suffixed_int(1, ""), Expr::int(1));
}
#[test]
fn a_selective_import_binds_one_name() {
let import = Import::selective(&["std", "typecons"], "Nullable");
assert_eq!(import.path.segments().len(), 2);
assert_eq!(import.symbols.len(), 1);
}
}