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 {
Types(TypeGroup),
Let(LetDecl),
ClassType(ClassTypeDecl),
Comment(Doc),
Verbatim(String),
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct TypeGroup {
pub doc: Doc,
pub items: Vec<TypeDecl>,
}
impl TypeGroup {
#[must_use]
pub fn one(item: TypeDecl) -> Self {
Self {
doc: Doc::none(),
items: alloc::vec![item],
}
}
#[must_use]
pub fn is_recursive_group(&self) -> bool {
self.items.len() > 1
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct TypeDecl {
pub doc: Doc,
pub name: Ident,
pub body: TypeBody,
pub body_text: BodyText,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum TypeBody {
Alias(Type),
Record(Vec<Field>),
Variant(Vec<Constructor>),
Abstract,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Field {
pub doc: Doc,
pub name: Ident,
pub type_: Type,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Constructor {
pub doc: Doc,
pub name: Ident,
pub args: Vec<Type>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct LetDecl {
pub doc: Doc,
pub name: Ident,
pub type_: Option<Type>,
pub body: Expr,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ClassTypeDecl {
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<Type>,
pub result: Type,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Type {
Primitive(Primitive),
Named(Ident),
Array(Box<Type>),
Option(Box<Type>),
Tuple(Vec<Type>),
Qualified {
module: Vec<Ident>,
name: Ident,
},
}
impl Type {
#[must_use]
pub fn named(text: &str) -> Self {
Self::Named(Ident::new(text))
}
#[must_use]
pub fn array(element: Self) -> Self {
Self::Array(Box::new(element))
}
#[must_use]
pub fn option(inner: Self) -> Self {
Self::Option(Box::new(inner))
}
#[must_use]
pub fn qualified(module: &str, name: &str) -> Self {
Self::Qualified {
module: alloc::vec![Ident::new(module)],
name: Ident::new(name),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Primitive {
Bool,
Int,
Int32,
Int64,
Float,
String,
Char,
Unit,
}
impl Primitive {
pub const ALL: [Self; 8] = [
Self::Bool,
Self::Int,
Self::Int32,
Self::Int64,
Self::Float,
Self::String,
Self::Char,
Self::Unit,
];
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::Bool => "bool",
Self::Int => "int",
Self::Int32 => "int32",
Self::Int64 => "int64",
Self::Float => "float",
Self::String => "string",
Self::Char => "char",
Self::Unit => "unit",
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Expr {
Literal(Literal),
Name(Ident),
Construct(Ident),
Apply {
func: Box<Expr>,
args: Vec<Expr>,
},
Function(Vec<Arm>),
Paren(Box<Expr>),
}
impl Expr {
#[must_use]
pub fn name(text: &str) -> Self {
Self::Name(Ident::new(text))
}
#[must_use]
pub fn construct(text: &str) -> Self {
Self::Construct(Ident::new(text))
}
#[must_use]
pub fn apply(func: Self, args: Vec<Self>) -> Self {
Self::Apply {
func: Box::new(func),
args,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Arm {
pub pattern: Pattern,
pub body: Expr,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Pattern {
Literal(Literal),
Construct(Ident),
Bind(Ident),
}
#[must_use]
pub fn int32_literal(value: i128) -> Literal {
Literal::Int(alloc::format!("{value}l"))
}
#[must_use]
pub fn int64_literal(value: i128) -> Literal {
Literal::Int(alloc::format!("{value}L"))
}
#[must_use]
pub fn lower_ident(name: &str) -> String {
let folded: String = name.chars().flat_map(char::to_lowercase).collect();
let mut out = match folded.chars().next() {
None => "zd_".to_string(),
Some('_') if folded.len() == 1 => "zd_".to_string(),
Some(first) if first == '_' || first.is_ascii_lowercase() => folded,
_ => {
let mut prefixed = String::from("zd_");
prefixed.push_str(&folded);
prefixed
}
};
if RESERVED.contains(&out.as_str()) {
out.push('_');
}
out
}
#[must_use]
pub fn upper_ident(name: &str) -> String {
let mut chars = name.chars();
let Some(first) = chars.next() else {
return "Zd".to_string();
};
if first.is_ascii_uppercase() {
return name.to_string();
}
if first.is_ascii_lowercase() {
let mut out = String::new();
for upper in first.to_uppercase() {
out.push(upper);
}
out.push_str(chars.as_str());
return out;
}
let mut out = String::from("Zd");
out.push_str(name);
out
}
pub const RESERVED: &[&str] = &[
"and",
"as",
"assert",
"asr",
"begin",
"class",
"constraint",
"do",
"done",
"downto",
"else",
"end",
"exception",
"external",
"false",
"for",
"fun",
"function",
"functor",
"if",
"in",
"include",
"inherit",
"initializer",
"land",
"lazy",
"let",
"lor",
"lsl",
"lsr",
"lxor",
"match",
"method",
"mod",
"module",
"mutable",
"new",
"nonrec",
"object",
"of",
"open",
"or",
"private",
"rec",
"sig",
"struct",
"then",
"to",
"true",
"try",
"type",
"val",
"virtual",
"when",
"while",
"with",
"array",
"bool",
"bytes",
"char",
"exn",
"float",
"format",
"int",
"int32",
"int64",
"lazy_t",
"list",
"nativeint",
"option",
"ref",
"result",
"string",
"unit",
];
pub const UNSIGNED_HOLDER: &str = "Zdwire.u64";
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ValueKind {
Int,
Int32,
Int64,
Float,
Bool,
Text,
UnitValue,
Enum(String),
ArrayOf(Box<ValueKind>),
OptionOf(Box<ValueKind>),
RecordOf(Vec<(String, ValueKind)>),
Aggregate,
Handle(&'static str),
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CodecType {
pub text: String,
pub kind: ValueKind,
pub default: String,
}
#[derive(Debug, Clone, PartialEq)]
pub enum CodeExpr {
Name(String),
Int(i128),
Int64Lit(i64),
BoolLit(bool),
TextLit(String),
FloatLit(String),
Runtime {
receiver: &'static str,
method: String,
args: Vec<CodeExpr>,
},
Static {
method: String,
args: Vec<CodeExpr>,
},
Call {
callee: String,
args: Vec<CodeExpr>,
},
Member {
base: String,
field: String,
case: bool,
},
OptionSlot {
base: String,
field: String,
},
OptionGet(Box<CodeExpr>),
IsSome {
base: String,
field: String,
},
Absent,
Whole(String),
Index {
base: Box<CodeExpr>,
index: Box<CodeExpr>,
},
EntryHalf {
entry: Box<CodeExpr>,
key: bool,
},
HeaderField {
value: Box<CodeExpr>,
field: String,
},
CountOf(String),
Elements(String),
Construct(String),
Binary {
op: &'static str,
lhs: Box<CodeExpr>,
rhs: Box<CodeExpr>,
},
Unary {
op: &'static str,
value: Box<CodeExpr>,
},
Paren(Box<CodeExpr>),
Convert {
target: CodecType,
value: Box<CodeExpr>,
},
RuntimeConst {
text: String,
kind: ValueKind,
},
Raw {
text: String,
kind: ValueKind,
},
}
#[derive(Debug, Clone, PartialEq)]
pub enum Stmt {
Let {
type_: CodecType,
name: String,
value: Option<CodeExpr>,
},
Scratch {
name: String,
},
Assign {
place: CodeExpr,
value: CodeExpr,
},
Store {
place: CodeExpr,
type_: CodecType,
value: CodeExpr,
},
ReadInto {
callee: String,
place: CodeExpr,
behaviour: CodeExpr,
},
Perform(CodeExpr),
Discard(CodeExpr),
Return,
When {
condition: CodeExpr,
then: Vec<Stmt>,
otherwise: Option<Vec<Stmt>>,
},
For {
counter: String,
bound: CodeExpr,
body: Vec<Stmt>,
},
While {
condition: CodeExpr,
body: Vec<Stmt>,
},
Leave,
Note(Doc),
Alloc {
base: String,
element: CodecType,
count: CodeExpr,
},
KeepFirst {
base: String,
count: CodeExpr,
},
}
#[derive(Debug, Clone, PartialEq)]
pub struct CodecFn {
pub doc: Doc,
pub name: String,
pub sample: CodecType,
pub reading: bool,
pub defaulter: Option<String>,
pub extra: Vec<(CodecType, String)>,
pub body: Vec<Stmt>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ValueFn {
pub doc: Doc,
pub name: String,
pub result: String,
pub body: String,
}
#[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_platform_dependent_width_a_member_can_reach() {
for primitive in Primitive::ALL {
assert_ne!(primitive.as_str(), "nativeint");
}
}
#[test]
fn a_type_name_folds_to_lower_case_because_the_grammar_says_so() {
assert_eq!(lower_ident("Pose"), "pose");
assert_eq!(lower_ident("pose"), "pose");
}
#[test]
fn the_whole_name_folds_and_not_only_the_first_letter() {
assert_eq!(lower_ident("READY"), "ready");
assert_eq!(lower_ident("MAX_LEN"), "max_len");
}
#[test]
fn a_keyword_takes_a_trailing_underscore() {
assert_eq!(lower_ident("type"), "type_");
assert_eq!(lower_ident("method"), "method_");
assert_eq!(lower_ident("end"), "end_");
assert_eq!(lower_ident("payload"), "payload");
}
#[test]
fn a_predefined_type_name_escapes_although_it_would_parse() {
for name in ["int", "string", "array", "option", "int32", "float"] {
assert_eq!(lower_ident(name), alloc::format!("{name}_"));
}
}
#[test]
fn a_leading_underscore_is_left_alone_because_the_grammar_admits_it() {
assert_eq!(lower_ident("_d"), "_d");
assert_eq!(lower_ident("_reserved"), "_reserved");
assert_eq!(lower_ident("_"), "zd_");
}
#[test]
fn a_constructor_capitalises_and_cannot_start_with_an_underscore() {
assert_eq!(upper_ident("IDLE"), "IDLE");
assert_eq!(upper_ident("idle"), "Idle");
assert_eq!(upper_ident("_value"), "Zd_value");
assert_eq!(upper_ident(""), "Zd");
}
#[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_int32_literal_carries_the_suffix_its_type_needs() {
assert_eq!(int32_literal(0), Literal::Int("0l".to_string()));
assert_eq!(int32_literal(-4), Literal::Int("-4l".to_string()));
assert_eq!(int64_literal(7), Literal::Int("7L".to_string()));
}
#[test]
fn a_group_of_one_is_not_a_recursive_group() {
let group = TypeGroup::one(TypeDecl {
body_text: crate::common::BodyText::none(),
doc: Doc::none(),
name: Ident::new("pose"),
body: TypeBody::Record(Vec::new()),
});
assert!(!group.is_recursive_group());
assert_eq!(group.items.len(), 1);
}
}