use crate::common::{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 modules: Vec<TopLevel>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum TopLevel {
Module(Module),
Verbatim(String),
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Module {
pub doc: Doc,
pub name: Path,
pub body: Vec<Item>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Item {
Attribute(Attribute),
Defstruct(Vec<Field>),
TypeAlias(TypeAlias),
Spec(Signature),
Callback(Signature),
Function(Function),
Verbatim(String),
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Attribute {
pub doc: Doc,
pub name: Ident,
pub value: Expr,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Field {
pub name: Ident,
pub default: Expr,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct TypeAlias {
pub doc: Doc,
pub name: Ident,
pub type_: Type,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Signature {
pub doc: Doc,
pub name: Ident,
pub params: Vec<Param>,
pub result: Type,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Param {
pub name: Option<Ident>,
pub type_: Type,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Function {
pub doc: Doc,
pub name: Ident,
pub params: Vec<Expr>,
pub body: Expr,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Type {
Basic(Basic),
Atom(String),
Local(Ident),
Remote {
module: Path,
name: Ident,
},
List(Box<Type>),
Tuple(Vec<Type>),
Struct(Vec<(Ident, Type)>),
Union(Vec<Type>),
}
impl Type {
#[must_use]
pub fn list(element: Self) -> Self {
Self::List(Box::new(element))
}
#[must_use]
pub fn or_nil(self) -> Self {
Self::Union(alloc::vec![self, Self::Basic(Basic::Nil)])
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Basic {
Integer,
Float,
Boolean,
Binary,
Nil,
None,
Any,
}
impl Basic {
pub const ALL: [Self; 7] = [
Self::Integer,
Self::Float,
Self::Boolean,
Self::Binary,
Self::Nil,
Self::None,
Self::Any,
];
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::Integer => "integer()",
Self::Float => "float()",
Self::Boolean => "boolean()",
Self::Binary => "binary()",
Self::Nil => "nil",
Self::None => "none()",
Self::Any => "any()",
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Expr {
Literal(Literal),
Atom(String),
Var(Ident),
Attr(Ident),
Module(Path),
List(Vec<Expr>),
Tuple(Vec<Expr>),
Keyword(Vec<(Ident, Expr)>),
Call {
func: Path,
args: Vec<Expr>,
},
Binary {
op: &'static str,
lhs: Box<Expr>,
rhs: Box<Expr>,
},
Unary {
op: &'static str,
value: Box<Expr>,
},
Paren(Box<Expr>),
Field {
value: Box<Expr>,
name: Ident,
},
Index {
value: Box<Expr>,
index: Box<Expr>,
},
IfElse {
condition: Box<Expr>,
then: Box<Expr>,
otherwise: Box<Expr>,
},
StructLiteral {
module: Path,
fields: Vec<(Ident, Expr)>,
},
}
impl Expr {
#[must_use]
pub fn int(value: i128) -> Self {
Self::Literal(Literal::int(value))
}
#[must_use]
pub fn string(text: &str) -> Self {
Self::Literal(Literal::string(text))
}
#[must_use]
pub fn nil() -> Self {
Self::Literal(Literal::Null)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Access {
Key(Ident),
At(Expr),
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Statement {
Bind {
name: Ident,
value: Expr,
},
BindTuple {
names: Vec<Ident>,
value: Expr,
},
Update {
base: Ident,
path: Vec<Access>,
value: Expr,
},
If {
condition: Expr,
then: Vec<Statement>,
otherwise: Vec<Statement>,
},
For {
counter: Ident,
bound: Expr,
body: Vec<Statement>,
},
While {
condition: Expr,
body: Vec<Statement>,
},
Break,
Return(Expr),
Comment(Doc),
Seq(Vec<Statement>),
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CodecFunction {
pub doc: Doc,
pub name: Ident,
pub params: Vec<Ident>,
pub body: Vec<Statement>,
pub result: Expr,
}
#[must_use]
pub fn atom_name(name: &str) -> String {
escape_name(&lower_first(name))
}
#[must_use]
pub fn module_segment(name: &str) -> String {
let raised = upper_first(name);
if RESERVED_MODULES.contains(&raised.as_str()) {
let mut out = raised;
out.push('_');
return out;
}
raised
}
#[must_use]
pub fn attribute_name(name: &str) -> String {
let lowered = atom_name(name);
if RESERVED_ATTRIBUTES.contains(&lowered.as_str()) {
let mut out = lowered;
out.push('_');
return out;
}
lowered
}
#[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()
}
fn lower_first(name: &str) -> String {
if !name.chars().any(char::is_lowercase) {
return name.to_lowercase();
}
let mut chars = name.chars();
let Some(first) = chars.next() else {
return String::new();
};
let mut out = String::new();
for lower in first.to_lowercase() {
out.push(lower);
}
out.push_str(chars.as_str());
out
}
fn upper_first(name: &str) -> String {
let mut chars = name.chars();
let Some(first) = chars.next() else {
return String::new();
};
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] = &[
"after",
"and",
"catch",
"do",
"else",
"end",
"false",
"fn",
"in",
"nil",
"not",
"or",
"rescue",
"true",
"when", "value",
"from_value",
];
pub const RESERVED_MODULES: &[&str] = &[
"Access",
"Agent",
"Application",
"ArgumentError",
"ArithmeticError",
"Atom",
"BadArityError",
"BadBooleanError",
"BadFunctionError",
"BadMapError",
"Base",
"Behaviour",
"Bitwise",
"Calendar",
"CaseClauseError",
"Code",
"Collectable",
"CompileError",
"CondClauseError",
"Config",
"Date",
"DateTime",
"Dict",
"Duration",
"DynamicSupervisor",
"EEx",
"Elixir",
"Enum",
"Enumerable",
"ErlangError",
"ExUnit",
"Exception",
"File",
"Float",
"Function",
"FunctionClauseError",
"GenEvent",
"GenServer",
"HashDict",
"HashSet",
"IEx",
"IO",
"Inspect",
"Integer",
"JSON",
"Kernel",
"KeyError",
"Keyword",
"List",
"Logger",
"Macro",
"Map",
"MapSet",
"MatchError",
"MismatchedDelimiterError",
"MissingApplicationsError",
"Mix",
"Module",
"NaiveDateTime",
"Node",
"OptionParser",
"PartitionSupervisor",
"Path",
"Port",
"Process",
"Protocol",
"Range",
"Record",
"Regex",
"Registry",
"RuntimeError",
"Set",
"Stream",
"String",
"StringIO",
"Supervisor",
"SyntaxError",
"System",
"SystemLimitError",
"Task",
"Time",
"TokenMissingError",
"TryClauseError",
"Tuple",
"URI",
"UndefinedFunctionError",
"UnicodeConversionError",
"Version",
"WithClauseError",
"ZeroddsWire",
];
pub const RESERVED_ATTRIBUTES: &[&str] = &[
"after_compile",
"before_compile",
"behaviour",
"callback",
"compile",
"deprecated",
"derive",
"dialyzer",
"doc",
"enforce_keys",
"external_resource",
"file",
"impl",
"macrocallback",
"moduledoc",
"nifs",
"on_definition",
"on_load",
"opaque",
"optional_callbacks",
"spec",
"type",
"typedoc",
"typep",
"vsn",
"zd_constants",
];
#[cfg(test)]
mod tests {
#![allow(clippy::expect_used, clippy::panic, clippy::unwrap_used)]
use super::*;
use alloc::format;
#[test]
fn every_basic_type_writes_its_own_spelling() {
let mut seen = Vec::new();
for basic in Basic::ALL {
let text = basic.as_str();
assert!(!seen.contains(&text), "{text} is used twice");
seen.push(text);
}
assert_eq!(seen.len(), Basic::ALL.len());
}
#[test]
fn nil_is_written_without_parentheses_and_the_rest_with_them() {
assert_eq!(Basic::Nil.as_str(), "nil");
for basic in Basic::ALL {
if basic != Basic::Nil {
assert!(basic.as_str().ends_with("()"), "{}", basic.as_str());
}
}
}
#[test]
fn an_identifier_with_mixed_case_lowers_its_first_character_only() {
assert_eq!(atom_name("active"), "active");
assert_eq!(atom_name("myField"), "myField");
assert_eq!(atom_name("Pose"), "pose");
}
#[test]
fn an_identifier_in_upper_case_throughout_is_lowered_throughout() {
assert_eq!(atom_name("IDLE"), "idle");
assert_eq!(atom_name("MAX_SIZE"), "max_size");
assert_eq!(atom_name("X"), "x");
}
#[test]
fn lowering_runs_before_the_escape_because_it_can_create_a_keyword() {
assert_eq!(atom_name("End"), "end_");
assert_eq!(atom_name("end"), "end_");
assert_eq!(atom_name("When"), "when_");
}
#[test]
fn the_functions_an_enumeration_module_defines_are_escaped_too() {
assert_eq!(atom_name("VALUE"), "value_");
assert_eq!(atom_name("from_value"), "from_value_");
}
#[test]
fn a_name_elixir_merely_auto_imports_is_left_alone() {
for name in ["node", "self", "length", "if", "use", "raise"] {
assert_eq!(atom_name(name), name);
}
}
#[test]
fn a_module_segment_raises_its_first_character() {
assert_eq!(module_segment("common"), "Common");
assert_eq!(module_segment("Location"), "Location");
assert_eq!(module_segment("myModule"), "MyModule");
}
#[test]
fn the_module_segment_elixir_is_escaped_because_it_is_the_root() {
assert_eq!(module_segment("elixir"), "Elixir_");
assert_eq!(module_segment("Elixir"), "Elixir_");
assert_eq!(module_segment("kernel"), "Kernel_");
}
#[test]
fn a_module_segment_the_elixir_distribution_already_names_is_escaped() {
assert_eq!(module_segment("Node"), "Node_");
assert_eq!(module_segment("node"), "Node_");
for name in ["Enum", "List", "Access", "String", "Map", "Range"] {
assert_eq!(module_segment(name), format!("{name}_"));
}
assert_eq!(module_segment("ZeroddsWire"), "ZeroddsWire_");
assert_eq!(module_segment("Task"), "Task_");
assert_eq!(module_segment("Supervised"), "Supervised");
}
#[test]
fn the_reserved_module_list_is_sorted_and_free_of_repeats() {
for pair in RESERVED_MODULES.windows(2) {
assert!(pair[0] < pair[1], "out of order: {} {}", pair[0], pair[1]);
}
}
#[test]
fn an_attribute_named_after_a_built_in_one_is_escaped() {
assert_eq!(attribute_name("doc"), "doc_");
assert_eq!(attribute_name("BEHAVIOUR"), "behaviour_");
assert_eq!(attribute_name("type"), "type_");
assert_eq!(attribute_name("LIMIT"), "limit");
}
#[test]
fn the_reserved_lists_have_no_duplicates() {
for list in [RESERVED, RESERVED_MODULES, RESERVED_ATTRIBUTES] {
let mut seen: Vec<&str> = Vec::new();
for name in list {
assert!(!seen.contains(name), "{name} is listed twice");
seen.push(name);
}
}
}
#[test]
fn every_reserved_word_escapes_to_something_that_is_not_reserved() {
for name in RESERVED {
let escaped = escape_name(name);
assert_eq!(escaped, format!("{name}_"));
assert!(!RESERVED.contains(&escaped.as_str()));
}
}
#[test]
fn a_module_name_keeps_its_segments_apart() {
let module = Module {
doc: Doc::none(),
name: Path::new(&["Common", "Util", "Location"]),
body: Vec::new(),
};
assert_eq!(module.name.segments().len(), 3);
}
#[test]
fn a_field_whose_type_has_no_zero_defaults_to_nil_and_keeps_its_place() {
let field = Field {
name: Ident::new("pose"),
default: Expr::nil(),
};
assert_eq!(field.default, Expr::Literal(Literal::Null));
}
#[test]
fn an_optional_type_is_the_type_or_nil() {
let type_ = Type::Basic(Basic::Integer).or_nil();
let Type::Union(arms) = &type_ else {
panic!("not a union");
};
assert_eq!(arms.len(), 2);
assert_eq!(arms[1], Type::Basic(Basic::Nil));
}
#[test]
fn an_array_type_nests_outermost_dimension_first() {
let inner = Type::Tuple(alloc::vec![Type::Basic(Basic::Integer); 3]);
let outer = Type::Tuple(alloc::vec![inner; 2]);
let Type::Tuple(rows) = &outer else {
panic!("not a tuple");
};
assert_eq!(rows.len(), 2);
let Type::Tuple(columns) = &rows[0] else {
panic!("the row is not a tuple");
};
assert_eq!(columns.len(), 3);
}
}