use crate::common::{BodyText, Doc, Ident, Literal};
use alloc::boxed::Box;
use alloc::string::{String, ToString};
use alloc::vec::Vec;
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct File {
pub doc: Doc,
pub decls: Vec<Decl>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Decl {
Const(ConstDecl),
Func(FuncDecl),
Comment(Doc),
Verbatim(String),
AnalyseAll(AnalyseAll),
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct FuncDecl {
pub doc: Doc,
pub is_pub: bool,
pub name: Ident,
pub params: Vec<FnParam>,
pub returns: Type,
pub body: Vec<Stmt>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct FnParam {
pub name: Ident,
pub type_: Type,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Stmt {
Var {
name: Ident,
type_: Option<Type>,
value: Option<Expr>,
mutable: bool,
},
Assign {
place: Expr,
value: Expr,
},
Expr(Expr),
DiscardExpr(Expr),
Discard(Ident),
Return(Option<Expr>),
If {
condition: Expr,
then: Vec<Stmt>,
otherwise: Option<Vec<Stmt>>,
},
CountUp {
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 ConstDecl {
pub doc: Doc,
pub is_pub: bool,
pub name: Ident,
pub type_: Option<Type>,
pub value: Expr,
pub body_text: BodyText,
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct AnalyseAll {
pub doc: Doc,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Field {
pub doc: Doc,
pub name: Ident,
pub type_: Type,
pub default: Option<Expr>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Variant {
pub doc: Doc,
pub name: Ident,
pub value: Option<Expr>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Param {
pub name: Option<Ident>,
pub type_: Type,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Type {
Primitive(Primitive),
Named(Ident),
Slice {
is_const: bool,
element: Box<Type>,
},
Array {
length: Box<Expr>,
element: Box<Type>,
},
Optional(Box<Type>),
Pointer {
is_const: bool,
target: Box<Type>,
},
Struct(Vec<Field>),
Enum {
tag: Box<Type>,
variants: Vec<Variant>,
},
Fn {
params: Vec<Param>,
returns: Box<Type>,
},
ErrorUnion {
error_set: Box<Type>,
payload: 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 {
is_const: false,
element: Box::new(element),
}
}
#[must_use]
pub fn const_slice(element: Self) -> Self {
Self::Slice {
is_const: true,
element: Box::new(element),
}
}
#[must_use]
pub fn text() -> Self {
Self::const_slice(Self::Primitive(Primitive::U8))
}
#[must_use]
pub fn array(length: i128, element: Self) -> Self {
Self::Array {
length: Box::new(Expr::Literal(Literal::int(length))),
element: Box::new(element),
}
}
#[must_use]
pub fn optional(inner: Self) -> Self {
Self::Optional(Box::new(inner))
}
#[must_use]
pub fn pointer(target: Self) -> Self {
Self::Pointer {
is_const: false,
target: Box::new(target),
}
}
#[must_use]
pub fn const_pointer(target: Self) -> Self {
Self::Pointer {
is_const: true,
target: Box::new(target),
}
}
#[must_use]
pub fn error_union(error_set: Self, payload: Self) -> Self {
Self::ErrorUnion {
error_set: Box::new(error_set),
payload: Box::new(payload),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Primitive {
Bool,
I8,
I16,
I32,
I64,
U8,
U16,
U32,
U64,
F32,
F64,
F128,
Void,
AnyOpaque,
}
impl Primitive {
pub const ALL: [Self; 14] = [
Self::Bool,
Self::I8,
Self::I16,
Self::I32,
Self::I64,
Self::U8,
Self::U16,
Self::U32,
Self::U64,
Self::F32,
Self::F64,
Self::F128,
Self::Void,
Self::AnyOpaque,
];
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::Bool => "bool",
Self::I8 => "i8",
Self::I16 => "i16",
Self::I32 => "i32",
Self::I64 => "i64",
Self::U8 => "u8",
Self::U16 => "u16",
Self::U32 => "u32",
Self::U64 => "u64",
Self::F32 => "f32",
Self::F64 => "f64",
Self::F128 => "f128",
Self::Void => "void",
Self::AnyOpaque => "anyopaque",
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Expr {
Literal(Literal),
Name(Ident),
Binary {
op: &'static str,
lhs: Box<Expr>,
rhs: Box<Expr>,
},
Paren(Box<Expr>),
Type(Box<Type>),
Call {
func: Box<Expr>,
args: Vec<Expr>,
},
Dot {
value: Box<Expr>,
name: Ident,
},
Index {
value: Box<Expr>,
index: Box<Expr>,
},
Unary {
op: &'static str,
value: Box<Expr>,
},
Try(Box<Expr>),
StructLit(Vec<(Ident, Expr)>),
ListLit(Vec<Expr>),
}
impl Expr {
#[must_use]
pub fn int(value: i128) -> Self {
Self::Literal(Literal::int(value))
}
#[must_use]
pub fn type_(type_: Type) -> Self {
Self::Type(Box::new(type_))
}
#[must_use]
pub fn call(callee: &str, args: Vec<Self>) -> Self {
Self::Call {
func: Box::new(Self::Name(Ident::new(callee))),
args,
}
}
#[must_use]
pub fn dot(value: Self, name: &str) -> Self {
Self::Dot {
value: Box::new(value),
name: Ident::new(name),
}
}
#[must_use]
pub fn try_(value: Self) -> Self {
Self::Try(Box::new(value))
}
#[must_use]
pub fn as_(type_: Type, value: Self) -> Self {
Self::call("@as", alloc::vec![Self::type_(type_), value])
}
}
#[must_use]
pub fn escape_field(name: &str) -> String {
if KEYWORDS.contains(&name) {
let mut out = name.to_string();
out.push('_');
return out;
}
name.to_string()
}
#[must_use]
pub fn escape_decl(name: &str) -> String {
if KEYWORDS.contains(&name) || is_primitive_name(name) {
let mut out = name.to_string();
out.push('_');
return out;
}
name.to_string()
}
#[must_use]
pub fn is_primitive_name(name: &str) -> bool {
if PRIMITIVE_NAMES.contains(&name) {
return true;
}
let Some(digits) = name.strip_prefix('u').or_else(|| name.strip_prefix('i')) else {
return false;
};
if digits.is_empty() || !digits.bytes().all(|byte| byte.is_ascii_digit()) {
return false;
}
digits == "0" || !digits.starts_with('0')
}
pub const KEYWORDS: &[&str] = &[
"addrspace",
"align",
"allowzero",
"and",
"anyframe",
"anytype",
"asm",
"async",
"await",
"break",
"callconv",
"catch",
"comptime",
"const",
"continue",
"defer",
"else",
"enum",
"errdefer",
"error",
"export",
"extern",
"fn",
"for",
"if",
"inline",
"linksection",
"noalias",
"noinline",
"nosuspend",
"opaque",
"or",
"orelse",
"packed",
"pub",
"resume",
"return",
"struct",
"suspend",
"switch",
"test",
"threadlocal",
"try",
"union",
"unreachable",
"usingnamespace",
"var",
"volatile",
"while",
];
pub const PRIMITIVE_NAMES: &[&str] = &[
"anyerror",
"anyframe",
"anyopaque",
"bool",
"c_char",
"c_int",
"c_long",
"c_longdouble",
"c_longlong",
"c_short",
"c_uint",
"c_ulong",
"c_ulonglong",
"c_ushort",
"comptime_float",
"comptime_int",
"f16",
"f32",
"f64",
"f80",
"f128",
"isize",
"noreturn",
"type",
"usize",
"void",
"false",
"null",
"true",
"undefined",
];
#[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 {
let text = primitive.as_str();
assert!(
!matches!(text, "isize" | "usize") && !text.starts_with("c_"),
"{text} has no fixed width"
);
}
}
#[test]
fn the_tree_can_write_a_binary128() {
assert!(Primitive::ALL.contains(&Primitive::F128));
assert_eq!(Primitive::F128.as_str(), "f128");
}
#[test]
fn a_keyword_escapes_in_both_positions() {
for name in ["align", "error", "test", "try", "fn", "comptime"] {
assert_eq!(escape_field(name), alloc::format!("{name}_"));
assert_eq!(escape_decl(name), alloc::format!("{name}_"));
}
}
#[test]
fn a_primitive_name_escapes_as_a_declaration_and_not_as_a_field() {
for name in ["type", "u8", "bool", "f128", "null", "usize"] {
assert_eq!(escape_field(name), name);
assert_eq!(escape_decl(name), alloc::format!("{name}_"));
}
}
#[test]
fn an_arbitrary_width_integer_is_a_primitive_name_too() {
for name in ["u0", "u1", "u7", "i17", "u65535"] {
assert!(is_primitive_name(name), "{name} is a Zig type");
}
for name in ["u01", "uX", "u", "i", "u1a", "unit", "index"] {
assert!(!is_primitive_name(name), "{name} is not a Zig type");
}
}
#[test]
fn a_plain_name_is_left_exactly_as_the_idl_wrote_it() {
for name in ["Pose", "pose", "x_min", "_d", "MODULE_ID"] {
assert_eq!(escape_field(name), name);
assert_eq!(escape_decl(name), name);
}
}
#[test]
fn neither_reserved_list_repeats_itself() {
for list in [KEYWORDS, PRIMITIVE_NAMES] {
let mut seen: Vec<&str> = Vec::new();
for name in list {
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::I32)));
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 text_is_a_const_slice_of_octets() {
let Type::Slice { is_const, element } = Type::text() else {
panic!("not a slice");
};
assert!(is_const, "a string literal does not coerce to `[]u8`");
assert_eq!(*element, Type::Primitive(Primitive::U8));
}
#[test]
fn an_optional_field_can_carry_its_absence_as_a_default() {
let field = Field {
doc: Doc::none(),
name: Ident::new("b"),
type_: Type::optional(Type::Primitive(Primitive::U8)),
default: Some(Expr::Literal(Literal::Null)),
};
assert_eq!(field.default, Some(Expr::Literal(Literal::Null)));
}
#[test]
fn a_type_is_a_value_so_one_declaration_form_carries_both() {
let as_type = ConstDecl {
body_text: crate::common::BodyText::none(),
doc: Doc::none(),
is_pub: true,
name: Ident::new("Pose"),
type_: None,
value: Expr::type_(Type::Struct(Vec::new())),
};
let as_constant = ConstDecl {
body_text: crate::common::BodyText::none(),
doc: Doc::none(),
is_pub: true,
name: Ident::new("LIMIT"),
type_: Some(Type::Primitive(Primitive::U8)),
value: Expr::int(8),
};
assert!(matches!(as_type.value, Expr::Type(_)));
assert!(as_constant.type_.is_some());
}
}