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 imports: Vec<Import>,
pub sections: Vec<Section>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Import {
pub path: String,
}
impl Import {
#[must_use]
pub fn new(path: &str) -> Self {
Self {
path: path.to_string(),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Section {
Type(Vec<TypeDecl>),
Const(Vec<ConstDecl>),
Comment(Doc),
Verbatim(String),
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct TypeDecl {
pub doc: Doc,
pub name: Ident,
pub exported: bool,
pub body: TypeBody,
pub text: TypeText,
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct TypeText {
pub before: Vec<String>,
pub body: BodyText,
pub after: Vec<String>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum TypeBody {
Object(Vec<Field>),
Enum(Vec<EnumField>),
Alias(Type),
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Field {
pub doc: Doc,
pub name: Ident,
pub exported: bool,
pub type_: Type,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct EnumField {
pub doc: Doc,
pub name: Ident,
pub value: Option<Expr>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ConstDecl {
pub doc: Doc,
pub name: Ident,
pub exported: bool,
pub type_: Option<Type>,
pub value: Expr,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Type {
Primitive(Primitive),
Named(Ident),
Seq(Box<Type>),
Array {
length: Box<Expr>,
element: Box<Type>,
},
Option(Box<Type>),
Ref(Box<Type>),
Proc {
params: Vec<Param>,
result: Option<Box<Type>>,
},
}
impl Type {
#[must_use]
pub fn named(text: &str) -> Self {
Self::Named(Ident::new(text))
}
#[must_use]
pub fn seq(element: Self) -> Self {
Self::Seq(Box::new(element))
}
#[must_use]
pub fn option(inner: Self) -> Self {
Self::Option(Box::new(inner))
}
#[must_use]
pub fn reference(target: Self) -> Self {
Self::Ref(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),
}
}
#[must_use]
pub fn needs_options(&self) -> bool {
match self {
Self::Option(_) => true,
Self::Seq(inner) | Self::Array { element: inner, .. } | Self::Ref(inner) => {
inner.needs_options()
}
Self::Proc { params, result } => {
params.iter().any(|param| param.type_.needs_options())
|| result.as_ref().is_some_and(|type_| type_.needs_options())
}
Self::Primitive(_) | Self::Named(_) => false,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Param {
pub name: Ident,
pub type_: Type,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Primitive {
Bool,
Int8,
Int16,
Int32,
Int64,
UInt8,
UInt16,
UInt32,
UInt64,
Float32,
Float64,
String,
Pointer,
}
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::Pointer,
];
#[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::Pointer => "pointer",
}
}
}
#[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>,
},
Dot {
value: Box<Expr>,
name: Ident,
},
Index {
value: Box<Expr>,
index: Box<Expr>,
},
Unary {
op: &'static str,
value: Box<Expr>,
},
Cast {
type_: Box<Type>,
value: Box<Expr>,
},
NewSeq {
element: Box<Type>,
length: Box<Expr>,
},
Deref(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 dot(value: Self, name: &str) -> Self {
Self::Dot {
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 enum Stmt {
Var {
name: Ident,
type_: Option<Type>,
value: Option<Expr>,
},
Assign {
place: Expr,
value: Expr,
},
Expr(Expr),
Discard(Expr),
Return(Option<Expr>),
If {
condition: Expr,
then: Vec<Stmt>,
otherwise: Option<Vec<Stmt>>,
},
For {
counter: Ident,
bound: Box<Expr>,
body: Vec<Stmt>,
},
While {
condition: Expr,
body: Vec<Stmt>,
},
Break,
Comment(Doc),
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ProcDecl {
pub doc: Doc,
pub name: Ident,
pub exported: bool,
pub params: Vec<Param>,
pub returns: Option<Type>,
pub body: Option<Vec<Stmt>>,
}
#[must_use]
pub fn normalise(name: &str) -> String {
let mut chars = name.chars();
let Some(first) = chars.next() else {
return String::new();
};
let mut out = String::new();
out.push(first);
for ch in chars {
if ch == '_' {
continue;
}
for lower in ch.to_lowercase() {
out.push(lower);
}
}
out
}
#[must_use]
pub fn escape_name(name: &str) -> String {
let repaired = repair(name);
if RESERVED.contains(&normalise(&repaired).as_str()) {
let mut out = String::from("`");
out.push_str(&repaired);
out.push('`');
return out;
}
repaired
}
const REPAIR_PREFIX: &str = "zd";
fn repair(name: &str) -> String {
let mut out = String::new();
let mut pending_underscore = false;
for ch in name.chars() {
if ch == '_' {
pending_underscore = !out.is_empty();
continue;
}
if pending_underscore {
out.push('_');
pending_underscore = false;
}
out.push(ch);
}
if !out
.chars()
.next()
.is_some_and(|ch| ch.is_ascii_alphabetic())
{
let mut prefixed = String::from(REPAIR_PREFIX);
prefixed.push_str(&out);
return prefixed;
}
out
}
pub const RESERVED: &[&str] = &[
"addr",
"and",
"as",
"asm",
"bind",
"block",
"break",
"case",
"cast",
"concept",
"const",
"continue",
"converter",
"defer",
"discard",
"distinct",
"div",
"do",
"elif",
"else",
"end",
"enum",
"except",
"export",
"finally",
"for",
"from",
"func",
"if",
"import",
"in",
"include",
"interface",
"is",
"isnot",
"iterator",
"let",
"macro",
"method",
"mixin",
"mod",
"nil",
"not",
"notin",
"object",
"of",
"or",
"out",
"proc",
"ptr",
"raise",
"ref",
"return",
"shl",
"shr",
"static",
"template",
"try",
"tuple",
"type",
"using",
"var",
"when",
"while",
"xor",
"yield",
];
pub const PREDECLARED: &[&str] = &[
"bool", "int8", "int16", "int32", "int64", "uint8", "uint16", "uint32", "uint64", "float32",
"float64", "string", "seq", "array",
"pointer", "options", "Option",
];
#[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" | "float"),
"{} has no fixed width, or hides the one it has",
primitive.as_str()
);
}
}
#[test]
fn normalisation_keeps_the_first_character_and_folds_the_rest() {
assert_eq!(normalise("fooBar"), "foobar");
assert_eq!(normalise("foo_bar"), "foobar");
assert_eq!(normalise("fooBAR"), "foobar");
assert_eq!(normalise("FooBar"), "Foobar");
assert_eq!(normalise(""), "");
}
#[test]
fn two_idl_spellings_that_nim_cannot_tell_apart_share_a_key() {
assert_eq!(normalise("foo_bar"), normalise("fooBar"));
assert_ne!(normalise("fooBar"), normalise("FooBar"));
}
#[test]
fn a_keyword_is_stropped_because_a_trailing_underscore_does_not_lex() {
assert_eq!(escape_name("type"), "`type`");
assert_eq!(escape_name("end"), "`end`");
assert_eq!(escape_name("proc"), "`proc`");
assert_eq!(escape_name("payload"), "payload");
}
#[test]
fn a_name_that_normalises_onto_a_keyword_is_stropped_too() {
assert_eq!(escape_name("t_ype"), "`t_ype`");
assert_eq!(escape_name("tYPE"), "`tYPE`");
assert_eq!(escape_name("Type"), "Type");
}
#[test]
fn a_leading_underscore_is_removed_because_nim_has_no_name_for_it() {
assert_eq!(escape_name("_d"), "d");
assert_eq!(escape_name("_reserved"), "reserved");
assert_eq!(escape_name("__x"), "x");
}
#[test]
fn a_trailing_or_doubled_underscore_is_removed_for_the_same_reason() {
assert_eq!(escape_name("count_"), "count");
assert_eq!(escape_name("a__b"), "a_b");
assert_eq!(escape_name("a_b"), "a_b");
assert_eq!(normalise("a__b"), normalise("a_b"));
}
#[test]
fn a_name_with_nothing_a_nim_identifier_may_start_with_takes_a_prefix() {
assert_eq!(escape_name("_"), "zd");
assert_eq!(escape_name("_1a"), "zd1a");
}
#[test]
fn the_reserved_list_has_no_duplicates_and_is_already_normalised() {
let mut seen: Vec<&str> = Vec::new();
for name in RESERVED {
assert!(!seen.contains(name), "{name} is listed twice");
assert_eq!(normalise(name), *name, "{name} is not in normal form");
seen.push(name);
}
}
#[test]
fn the_predeclared_list_has_no_duplicates_and_no_keywords() {
let mut seen: Vec<&str> = Vec::new();
for name in PREDECLARED {
assert!(!seen.contains(name), "{name} is listed twice");
assert!(!RESERVED.contains(name), "{name} is a keyword");
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 an_option_anywhere_inside_a_type_asks_for_the_import() {
assert!(Type::option(Type::Primitive(Primitive::Int32)).needs_options());
assert!(Type::seq(Type::option(Type::Primitive(Primitive::Bool))).needs_options());
assert!(!Type::seq(Type::Primitive(Primitive::Bool)).needs_options());
assert!(Type::option(Type::reference(Type::named("Node"))).needs_options());
assert!(!Type::reference(Type::named("Node")).needs_options());
assert!(
Type::Proc {
params: Vec::new(),
result: Some(Box::new(Type::option(Type::Primitive(Primitive::Int32)))),
}
.needs_options()
);
}
#[test]
fn an_enumeration_field_has_no_export_marker_to_set() {
let field = EnumField {
doc: Doc::none(),
name: Ident::new("IDLE"),
value: Some(Expr::int(0)),
};
assert_eq!(field.name.as_str(), "IDLE");
}
}