use crate::common::{BodyText, Doc, Ident, Literal};
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 package: Ident,
pub imports: Vec<Import>,
pub decls: Vec<Decl>,
}
impl Default for File {
fn default() -> Self {
Self {
doc: Doc::none(),
package: Ident::new("zdtypes"),
imports: Vec::new(),
decls: Vec::new(),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Import {
pub path: String,
pub alias: Option<Ident>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Decl {
Type(TypeDecl),
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 results: Vec<Type>,
pub body: Vec<Stmt>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct TypeDecl {
pub doc: Doc,
pub name: Ident,
pub alias: bool,
pub type_: Type,
pub body_text: BodyText,
}
#[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 params: Vec<Param>,
pub results: Vec<Type>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Param {
pub name: Option<Ident>,
pub type_: Type,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Field {
pub doc: Doc,
pub name: Ident,
pub type_: Type,
pub tag: Option<Tag>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Tag {
pub entries: Vec<(String, String)>,
}
impl Tag {
#[must_use]
pub fn one(key: &str, value: &str) -> Self {
Self {
entries: alloc::vec![(key.to_string(), value.to_string())],
}
}
}
#[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>),
Struct(Vec<Field>),
}
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 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 {
Bool,
Int8,
Int16,
Int32,
Int64,
UInt8,
UInt16,
UInt32,
UInt64,
Float32,
Float64,
String,
Any,
}
impl Primitive {
pub const ALL: [Self; 13] = [
Self::Bool,
Self::Int8,
Self::Int16,
Self::Int32,
Self::Int64,
Self::UInt8,
Self::UInt16,
Self::UInt32,
Self::UInt64,
Self::Float32,
Self::Float64,
Self::String,
Self::Any,
];
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::Bool => "bool",
Self::Int8 => "int8",
Self::Int16 => "int16",
Self::Int32 => "int32",
Self::Int64 => "int64",
Self::UInt8 => "uint8",
Self::UInt16 => "uint16",
Self::UInt32 => "uint32",
Self::UInt64 => "uint64",
Self::Float32 => "float32",
Self::Float64 => "float64",
Self::String => "string",
Self::Any => "any",
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Expr {
Literal(Literal),
Name(Ident),
Binary {
op: &'static str,
lhs: Box<Expr>,
rhs: Box<Expr>,
},
Convert {
type_: Box<Type>,
value: Box<Expr>,
},
Paren(Box<Expr>),
Call {
func: Box<Expr>,
args: Vec<Expr>,
},
Selector {
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>,
},
Composite {
type_: Option<Box<Type>>,
elems: Vec<CompositeElem>,
},
Make {
type_: Box<Type>,
length: Box<Expr>,
},
}
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(Ident::new(text))
}
#[must_use]
pub fn selector(value: Self, name: &str) -> Self {
Self::Selector {
value: Box::new(value),
name: Ident::new(name),
}
}
#[must_use]
pub fn call(func: Self, args: Vec<Self>) -> Self {
Self::Call {
func: Box::new(func),
args,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CompositeElem {
pub key: Option<Ident>,
pub value: Expr,
}
#[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,
init: Box<Expr>,
bound: Box<Expr>,
body: Vec<Stmt>,
},
While {
condition: Expr,
body: Vec<Stmt>,
},
Break,
Comment(Doc),
}
#[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()
}
#[must_use]
pub fn exported(name: &str) -> String {
let mut chars = name.chars();
let Some(first) = chars.next() else {
return String::new();
};
if !first.is_lowercase() {
return name.to_string();
}
let mut out = String::new();
for upper in first.to_uppercase() {
out.push(upper);
}
out.push_str(chars.as_str());
out
}
pub const RESERVED: &[&str] = &[
"break",
"case",
"chan",
"const",
"continue",
"default",
"defer",
"else",
"fallthrough",
"for",
"func",
"go",
"goto",
"if",
"import",
"interface",
"map",
"package",
"range",
"return",
"select",
"struct",
"switch",
"type",
"var",
"any",
"bool",
"byte",
"comparable",
"complex64",
"complex128",
"error",
"float32",
"float64",
"int",
"int8",
"int16",
"int32",
"int64",
"rune",
"string",
"uint",
"uint8",
"uint16",
"uint32",
"uint64",
"uintptr",
"true",
"false",
"iota",
"nil",
"append",
"cap",
"clear",
"close",
"complex",
"copy",
"delete",
"imag",
"len",
"make",
"max",
"min",
"new",
"panic",
"print",
"println",
"real",
"recover",
];
#[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_an_implementation_defined_width() {
for primitive in Primitive::ALL {
assert!(
!matches!(primitive.as_str(), "int" | "uint" | "uintptr"),
"{} has no fixed width",
primitive.as_str()
);
}
}
#[test]
fn a_keyword_escapes_with_a_trailing_underscore() {
assert_eq!(escape_name("type"), "type_");
assert_eq!(escape_name("range"), "range_");
assert_eq!(escape_name("payload"), "payload");
}
#[test]
fn a_predeclared_name_escapes_although_it_would_parse() {
for name in ["string", "int32", "len", "any", "error", "min"] {
assert_eq!(escape_name(name), alloc::format!("{name}_"));
}
}
#[test]
fn exporting_moves_the_first_letter_and_nothing_else() {
assert_eq!(exported("value"), "Value");
assert_eq!(exported("Value"), "Value");
assert_eq!(exported("aBc"), "ABc");
assert_eq!(exported(""), "");
}
#[test]
fn a_leading_underscore_is_left_alone_because_go_cannot_export_it() {
assert_eq!(exported("_d"), "_d");
assert_eq!(exported("_reserved"), "_reserved");
}
#[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 an_array_type_nests_outermost_dimension_first() {
let type_ = Type::array(2, Type::array(3, Type::Primitive(Primitive::Int32)));
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_tag() {
let field = Field {
doc: Doc::none(),
name: Ident::new("Type_"),
type_: Type::Primitive(Primitive::Int32),
tag: Some(Tag::one("idl", "type")),
};
let tag = field.tag.expect("the tag");
assert_eq!(tag.entries.len(), 1);
assert_eq!(tag.entries[0].1, "type");
}
}