use crate::common::{BodyText, Doc, Ident, Literal, Path};
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 package: Option<Path>,
pub imports: Vec<Path>,
pub decls: Vec<Decl>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Decl {
Class(Class),
Object(Object),
Alias(AliasDecl),
Const(ConstDecl),
Func(FuncDecl),
Interface(Class),
Comment(Doc),
Verbatim(String),
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Class {
pub doc: Doc,
pub is_data: bool,
pub name: Ident,
pub properties: Vec<Property>,
pub body_text: BodyText,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Property {
pub doc: Doc,
pub mutable: bool,
pub name: Ident,
pub type_: Type,
pub default: Option<Expr>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Object {
pub doc: Doc,
pub name: Ident,
pub members: Vec<ConstDecl>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct AliasDecl {
pub doc: Doc,
pub name: Ident,
pub target: Type,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ConstDecl {
pub doc: Doc,
pub is_const: bool,
pub name: Ident,
pub type_: Type,
pub value: Expr,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct FuncDecl {
pub doc: Doc,
pub name: Ident,
pub params: Vec<Param>,
pub returns: Type,
pub body: Vec<Stmt>,
}
#[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 {
path: Path,
args: Vec<Type>,
},
Array(Box<Type>),
Nullable(Box<Type>),
Function {
params: Vec<Type>,
returns: Box<Type>,
},
Unit,
}
impl Type {
#[must_use]
pub fn named(text: &str) -> Self {
Self::Named {
path: Path::single(text),
args: Vec::new(),
}
}
#[must_use]
pub fn generic1(text: &str, arg: Self) -> Self {
Self::Named {
path: Path::single(text),
args: alloc::vec![arg],
}
}
#[must_use]
pub fn array(element: Self) -> Self {
Self::Array(Box::new(element))
}
#[must_use]
pub fn nullable(inner: Self) -> Self {
Self::Nullable(Box::new(inner))
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Primitive {
Boolean,
Byte,
Short,
Int,
Long,
Float,
Double,
Char,
}
impl Primitive {
pub const ALL: [Self; 8] = [
Self::Boolean,
Self::Byte,
Self::Short,
Self::Int,
Self::Long,
Self::Float,
Self::Double,
Self::Char,
];
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::Boolean => "Boolean",
Self::Byte => "Byte",
Self::Short => "Short",
Self::Int => "Int",
Self::Long => "Long",
Self::Float => "Float",
Self::Double => "Double",
Self::Char => "Char",
}
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct Block {
pub statements: Vec<Stmt>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Stmt {
Declare {
mutable: bool,
type_: Type,
name: Ident,
value: Option<Expr>,
},
Expr(Expr),
Return(Option<Expr>),
If {
condition: Expr,
then: Block,
otherwise: Option<Block>,
},
CountUp {
counter: Ident,
bound: Box<Expr>,
body: Block,
},
While {
condition: Expr,
body: Block,
},
Break,
Throw(Expr),
Comment(Doc),
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Expr {
Literal(Literal),
Path(Path),
Call {
receiver: Option<Box<Expr>>,
name: Ident,
args: Vec<Expr>,
},
Field {
value: Box<Expr>,
name: Ident,
},
Index {
value: Box<Expr>,
index: Box<Expr>,
},
New {
type_: Box<Type>,
args: Vec<Expr>,
},
NewArray {
element: Box<Type>,
length: Box<Expr>,
init: Option<Box<Expr>>,
},
ArrayOf {
element: Box<Type>,
elements: Vec<Expr>,
},
Binary {
op: &'static str,
lhs: Box<Expr>,
rhs: Box<Expr>,
},
Paren(Box<Expr>),
As {
value: Box<Expr>,
type_: Box<Type>,
},
If {
condition: Box<Expr>,
then: Box<Expr>,
otherwise: Box<Expr>,
},
Not(Box<Expr>),
NotNull(Box<Expr>),
}
impl Expr {
#[must_use]
pub fn name(text: &str) -> Self {
Self::Path(Path::single(text))
}
#[must_use]
pub fn int(value: i128) -> Self {
Self::Literal(Literal::int(value))
}
#[must_use]
pub fn field(value: Self, name: &str) -> Self {
Self::Field {
value: Box::new(value),
name: Ident::new(name),
}
}
#[must_use]
pub fn call(name: &str, args: Vec<Self>) -> Self {
Self::Call {
receiver: None,
name: Ident::new(name),
args,
}
}
#[must_use]
pub fn method_call(receiver: Self, name: &str, args: Vec<Self>) -> Self {
Self::Call {
receiver: Some(Box::new(receiver)),
name: Ident::new(name),
args,
}
}
}
#[must_use]
pub fn escape_identifier(name: &str) -> String {
if HARD_KEYWORDS.contains(&name) {
let mut out = name.to_string();
out.push('_');
return out;
}
name.to_string()
}
pub const HARD_KEYWORDS: &[&str] = &[
"as",
"break",
"class",
"continue",
"do",
"else",
"false",
"for",
"fun",
"if",
"in",
"interface",
"is",
"null",
"object",
"package",
"return",
"super",
"this",
"throw",
"true",
"try",
"typealias",
"typeof",
"val",
"var",
"when",
"while",
];
#[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_unsigned_primitive_exists() {
for primitive in Primitive::ALL {
assert!(!primitive.as_str().starts_with('U'), "{primitive:?}");
}
}
#[test]
fn a_hard_keyword_escapes_with_a_trailing_underscore() {
for name in ["class", "object", "val", "fun", "when"] {
assert_eq!(escape_identifier(name), alloc::format!("{name}_"));
}
}
#[test]
fn a_soft_keyword_is_not_escaped() {
for name in ["data", "field", "import", "value", "constructor"] {
assert_eq!(escape_identifier(name), name);
}
}
#[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_identifier(name), name);
}
}
#[test]
fn hard_keywords_do_not_repeat() {
let mut seen: Vec<&str> = Vec::new();
for name in HARD_KEYWORDS {
assert!(!seen.contains(name), "{name} is listed twice");
seen.push(name);
}
}
#[test]
fn a_property_can_carry_the_default_that_makes_the_zero_argument_constructor_work() {
let property = Property {
doc: Doc::none(),
mutable: true,
name: Ident::new("x"),
type_: Type::Primitive(Primitive::Int),
default: Some(Expr::int(0)),
};
assert_eq!(property.default, Some(Expr::int(0)));
}
#[test]
fn an_array_of_a_primitive_and_of_a_named_type_are_both_expressible() {
let ints = Type::array(Type::Primitive(Primitive::Int));
let Type::Array(element) = &ints else {
panic!("not an array");
};
assert_eq!(**element, Type::Primitive(Primitive::Int));
let names = Type::array(Type::named("Pose"));
assert_eq!(names, Type::Array(Box::new(Type::named("Pose"))));
}
}