use crate::writer::{Writer, comment_lines, verbatim_lines};
use alloc::string::{String, ToString};
use alloc::vec::Vec;
use zerodds_lang_cst::common::{Doc, Ident, Literal};
use zerodds_lang_cst::fsharp::{
ClassField, Decl, Expr, File, FnParam, LetDecl, Primitive, Stmt, Type, TypeBody, TypeDecl,
Variant,
};
#[must_use]
pub fn file(file: &File) -> String {
let mut w = Writer::spaces4();
if !file.doc.is_empty() {
comment_lines(&mut w, "// ", file.doc.as_lines());
}
let mut head = String::from("module rec ");
head.push_str(file.module.as_str());
w.line(&head);
for decl in &file.decls {
w.blank_line();
render_decl(&mut w, decl);
}
w.finish()
}
#[must_use]
pub fn decl(node: &Decl) -> String {
let mut w = Writer::spaces4();
render_decl(&mut w, node);
w.finish()
}
#[must_use]
pub fn type_(node: &Type) -> String {
type_text(node)
}
#[must_use]
pub fn expr(node: &Expr) -> String {
expr_text(node)
}
#[must_use]
pub fn ident(name: &Ident) -> String {
name.as_str().to_string()
}
fn render_decl(w: &mut Writer, node: &Decl) {
match node {
Decl::Type(decl) => render_type_decl(w, decl),
Decl::Let(decl) => render_let(w, decl),
Decl::Comment(lines) => comment_lines(w, "// ", lines.as_lines()),
Decl::Verbatim(text) => verbatim_lines(w, text),
}
}
fn render_type_decl(w: &mut Writer, node: &TypeDecl) {
doc(w, &node.doc);
match &node.body {
TypeBody::Alias(target) => {
let mut line = String::from("type ");
line.push_str(node.name.as_str());
line.push_str(" = ");
line.push_str(&type_text(target));
w.line(&line);
}
TypeBody::Class(fields) => {
let mut head = String::from("type ");
head.push_str(node.name.as_str());
head.push_str("() =");
w.line(&head);
w.indent();
let text = &node.body_text;
let has_text = !text.begin.is_empty() || !text.end.is_empty();
if fields.is_empty() && !has_text {
w.line("class end");
} else if fields.is_empty() {
w.line("class");
w.indent();
for line in text.begin.iter().chain(&text.end) {
verbatim_lines(w, line);
}
w.dedent();
w.line("end");
} else {
for line in &text.begin {
verbatim_lines(w, line);
}
for field in fields {
render_class_field(w, field);
}
for line in &text.end {
verbatim_lines(w, line);
}
}
w.dedent();
}
TypeBody::Enum { backing, variants } => {
let mut head = String::from("type ");
head.push_str(node.name.as_str());
head.push_str(" =");
w.line(&head);
w.indent();
for variant in variants {
render_variant(w, backing, variant);
}
w.dedent();
}
}
}
fn render_class_field(w: &mut Writer, node: &ClassField) {
doc(w, &node.doc);
let mut line = String::from("member val ");
line.push_str(node.name.as_str());
line.push_str(" : ");
line.push_str(&type_text(&node.type_));
line.push_str(" = ");
line.push_str(&expr_text(&node.default));
line.push_str(" with get, set");
w.line(&line);
}
fn render_variant(w: &mut Writer, backing: &Type, node: &Variant) {
let _ = backing;
doc(w, &node.doc);
let mut line = String::from("| ");
line.push_str(node.name.as_str());
line.push_str(" = ");
line.push_str(&expr_text(&node.value));
w.line(&line);
}
fn render_let(w: &mut Writer, node: &LetDecl) {
doc(w, &node.doc);
let mut head = String::from("let ");
head.push_str(node.name.as_str());
if node.params.is_empty() {
if let Some(type_) = &node.type_ {
head.push_str(" : ");
head.push_str(&type_text(type_));
}
} else {
for param in &node.params {
head.push_str(" (");
head.push_str(param_text(param).as_str());
head.push(')');
}
if let Some(type_) = &node.type_ {
head.push_str(" : ");
head.push_str(&type_text(type_));
}
}
head.push_str(" =");
if let [Stmt::Return(value)] = node.body.as_slice() {
head.push(' ');
head.push_str(&expr_text(value));
w.line(&head);
return;
}
w.line(&head);
w.indent();
for statement in &node.body {
render_stmt(w, statement);
}
w.dedent();
}
fn param_text(param: &FnParam) -> String {
let mut out = String::from(param.name.as_str());
out.push_str(": ");
out.push_str(&type_text(¶m.type_));
out
}
fn render_stmt(w: &mut Writer, node: &Stmt) {
match node {
Stmt::Let {
name,
type_,
value,
mutable,
} => {
let mut line = String::from(if *mutable { "let mutable " } else { "let " });
line.push_str(name.as_str());
if let Some(type_) = type_ {
line.push_str(" : ");
line.push_str(&type_text(type_));
}
line.push_str(" = ");
line.push_str(&expr_text(value));
w.line(&line);
}
Stmt::Assign { place, value } => {
let mut line = expr_text(place);
line.push_str(" <- ");
line.push_str(&expr_text(value));
w.line(&line);
}
Stmt::Expr(value) => w.line(&expr_text(value)),
Stmt::If { .. } => render_if(w, node, "if "),
Stmt::CountUp {
counter,
type_,
bound,
body,
} => {
let mut start = String::from("for ");
start.push_str(counter.as_str());
start.push_str(" = ");
start.push_str(&zero_of(type_));
start.push_str(" to (");
start.push_str(&expr_text(bound));
start.push_str(") - ");
start.push_str(&one_of(type_));
start.push_str(" do");
w.line(&start);
w.indent();
for statement in body {
render_stmt(w, statement);
}
w.dedent();
}
Stmt::Until { condition, body } => {
w.line("try");
w.indent();
let mut head = String::from("while ");
head.push_str(&expr_text(condition));
head.push_str(" do");
w.line(&head);
w.indent();
for statement in body {
render_stmt(w, statement);
}
w.dedent();
w.dedent();
w.line("with");
w.indent();
w.line(":? ZdWire.LoopBreak -> ()");
w.dedent();
}
Stmt::Leave => w.line("raise (ZdWire.LoopBreak())"),
Stmt::GiveBack => w.line("raise (ZdWire.GiveBack())"),
Stmt::Guarded { body } => {
w.line("try");
w.indent();
for statement in body {
render_stmt(w, statement);
}
w.dedent();
w.line("with");
w.indent();
w.line(":? ZdWire.GiveBack -> ()");
w.dedent();
}
Stmt::Comment(lines) => comment_lines(w, "// ", lines.as_lines()),
Stmt::Return(value) => w.line(&expr_text(value)),
}
}
fn zero_of(type_: &Type) -> String {
if *type_ == Type::Primitive(Primitive::UInt32) {
"0u".to_string()
} else {
"0".to_string()
}
}
fn one_of(type_: &Type) -> String {
if *type_ == Type::Primitive(Primitive::UInt32) {
"1u".to_string()
} else {
"1".to_string()
}
}
fn render_if(w: &mut Writer, node: &Stmt, keyword: &str) {
let mut node = node;
let mut keyword = keyword;
loop {
let Stmt::If {
condition,
then,
otherwise,
} = node
else {
return;
};
let mut head = String::from(keyword);
head.push_str(&expr_text(condition));
head.push_str(" then");
w.line(&head);
w.indent();
if then.is_empty() {
w.line("()");
}
for statement in then {
render_stmt(w, statement);
}
w.dedent();
match otherwise {
None => return,
Some(chain) if chain.len() == 1 && matches!(chain[0], Stmt::If { .. }) => {
node = &chain[0];
keyword = "elif ";
}
Some(block) => {
w.line("else");
w.indent();
if block.is_empty() {
w.line("()");
}
for statement in block {
render_stmt(w, statement);
}
w.dedent();
return;
}
}
}
}
fn type_text(node: &Type) -> String {
match node {
Type::Primitive(primitive) => primitive.as_str().to_string(),
Type::Named(name) => name.as_str().to_string(),
Type::Array(element) => {
let mut out = type_text(element);
out.push_str(" []");
out
}
Type::Option(inner) => {
let mut out = type_text(inner);
out.push_str(" option");
out
}
Type::Fn { params, returns } => {
if params.is_empty() {
let mut out = String::from("unit -> ");
out.push_str(&type_text(returns));
return out;
}
let parts: Vec<String> = params.iter().map(type_text).collect();
let mut out = parts.join(" -> ");
out.push_str(" -> ");
out.push_str(&type_text(returns));
out
}
}
}
fn expr_text(node: &Expr) -> String {
match node {
Expr::Literal(literal) => literal_text(literal),
Expr::Name(name) => name.as_str().to_string(),
Expr::Binary { op, lhs, rhs } => {
let mut out = expr_text(lhs);
out.push(' ');
out.push_str(op);
out.push(' ');
out.push_str(&expr_text(rhs));
out
}
Expr::Paren(inner) => {
let mut out = String::from("(");
out.push_str(&expr_text(inner));
out.push(')');
out
}
Expr::Call { func, args } => {
let mut out = expr_text(func);
out.push('(');
let parts: Vec<String> = args.iter().map(expr_text).collect();
out.push_str(&parts.join(", "));
out.push(')');
out
}
Expr::CurriedCall { func, args } => {
let mut out = expr_text(func);
if args.is_empty() {
out.push_str(" ()");
return out;
}
for arg in args {
out.push_str(" (");
out.push_str(&expr_text(arg));
out.push(')');
}
out
}
Expr::Dot { value, name } => {
let mut out = expr_text(value);
out.push('.');
out.push_str(name.as_str());
out
}
Expr::Index { value, index } => {
let mut out = expr_text(value);
out.push_str(".[");
out.push_str(&expr_text(index));
out.push(']');
out
}
Expr::Unary { op, value } => {
let mut out = String::from(*op);
out.push_str(&expr_text(value));
out
}
Expr::Some_(inner) => {
let mut out = String::from("Some (");
out.push_str(&expr_text(inner));
out.push(')');
out
}
Expr::None_ => "None".to_string(),
Expr::New { type_, args } => {
let mut out = type_.as_str().to_string();
out.push('(');
let parts: Vec<String> = args.iter().map(expr_text).collect();
out.push_str(&parts.join(", "));
out.push(')');
out
}
Expr::ObjectInit { type_, fields } => {
let mut out = type_.as_str().to_string();
out.push('(');
let parts: Vec<String> = fields
.iter()
.map(|(name, value)| {
let mut part = name.as_str().to_string();
part.push_str(" = ");
part.push_str(&expr_text(value));
part
})
.collect();
out.push_str(&parts.join(", "));
out.push(')');
out
}
Expr::ArrayLit(elements) => {
if elements.is_empty() {
return "[||]".to_string();
}
let mut out = String::from("[| ");
let parts: Vec<String> = elements.iter().map(expr_text).collect();
out.push_str(&parts.join("; "));
out.push_str(" |]");
out
}
Expr::ArrayZeroCreate { element, count } => {
let mut out = String::from("Array.zeroCreate<");
out.push_str(&type_text(element));
out.push_str("> (");
out.push_str(&expr_text(count));
out.push(')');
out
}
}
}
fn literal_text(literal: &Literal) -> String {
match literal {
Literal::Int(text) | Literal::Float(text) => text.clone(),
Literal::Bool(value) => {
if *value {
"true".to_string()
} else {
"false".to_string()
}
}
Literal::Char(ch) => {
alloc::format!("{}", *ch as u32)
}
Literal::Str(text) => {
let mut out = String::from("\"");
out.push_str(&string_text(text));
out.push('"');
out
}
Literal::Bytes(bytes) => {
let mut out = String::from("[| ");
let parts: Vec<String> = bytes.iter().map(|b| alloc::format!("{b}uy")).collect();
out.push_str(&parts.join("; "));
out.push_str(" |]");
out
}
Literal::Null => "None".to_string(),
}
}
#[must_use]
pub fn string_text(text: &str) -> String {
crate::writer::escape_string(text)
}
fn doc(w: &mut Writer, doc: &Doc) {
if doc.is_empty() {
return;
}
comment_lines(w, "/// ", doc.as_lines());
}
#[cfg(test)]
mod tests {
#![allow(clippy::expect_used, clippy::panic, clippy::unwrap_used)]
use super::*;
use alloc::boxed::Box;
use zerodds_lang_cst::fsharp::Primitive;
fn class_decl(fields: Vec<ClassField>) -> Decl {
Decl::Type(TypeDecl {
body_text: zerodds_lang_cst::common::BodyText::none(),
doc: Doc::none(),
name: Ident::new("Pose"),
body: TypeBody::Class(fields),
})
}
fn field(name: &str, type_: Type, default: Expr) -> ClassField {
ClassField {
doc: Doc::none(),
name: Ident::new(name),
type_,
default,
}
}
#[test]
fn a_class_declaration_indents_by_four_and_writes_member_val() {
let text = decl(&class_decl(alloc::vec![
field("X", Type::Primitive(Primitive::Int32), Expr::int(0)),
field(
"Y",
Type::Primitive(Primitive::Double),
Expr::Literal(Literal::Float("0.0".to_string())),
),
]));
assert_eq!(
text,
"type Pose() =\n member val X : int32 = 0 with get, set\n \
member val Y : float = 0.0 with get, set\n"
);
}
#[test]
fn an_empty_class_writes_class_end() {
assert_eq!(
decl(&class_decl(Vec::new())),
"type Pose() =\n class end\n"
);
}
#[test]
fn a_type_reads_left_to_right_with_no_declarator_to_split() {
assert_eq!(
type_(&Type::array(Type::array(Type::Primitive(Primitive::Int32)))),
"int32 [] []"
);
assert_eq!(type_(&Type::text()), "string");
assert_eq!(type_(&Type::array(Type::named("Entry"))), "Entry []");
assert_eq!(
type_(&Type::option(Type::Primitive(Primitive::Byte))),
"byte option"
);
}
#[test]
fn an_enum_writes_its_backing_type_once_and_every_value() {
let text = decl(&Decl::Type(TypeDecl {
body_text: zerodds_lang_cst::common::BodyText::none(),
doc: Doc::none(),
name: Ident::new("Mode"),
body: TypeBody::Enum {
backing: Type::Primitive(Primitive::Int32),
variants: alloc::vec![
Variant {
doc: Doc::none(),
name: Ident::new("IDLE"),
value: Expr::int(0),
},
Variant {
doc: Doc::none(),
name: Ident::new("ACTIVE"),
value: Expr::int(4),
},
],
},
}));
assert_eq!(text, "type Mode =\n | IDLE = 0\n | ACTIVE = 4\n");
}
#[test]
fn a_constant_writes_its_ascription_before_its_value() {
let text = decl(&Decl::Let(LetDecl {
doc: Doc::none(),
name: Ident::new("LIMIT"),
params: Vec::new(),
type_: Some(Type::Primitive(Primitive::Byte)),
body: alloc::vec![Stmt::Return(Expr::int(8))],
}));
assert_eq!(text, "let LIMIT : byte = 8\n");
}
#[test]
fn a_function_writes_its_parameters_curried() {
let text = decl(&Decl::Let(LetDecl {
doc: Doc::none(),
name: Ident::new("zd_enc_Pose"),
params: alloc::vec![
FnParam {
name: Ident::new("w"),
type_: Type::named("ZdWire.Writer"),
},
FnParam {
name: Ident::new("v"),
type_: Type::named("Pose"),
},
],
type_: Some(Type::Primitive(Primitive::Unit)),
body: alloc::vec![Stmt::Return(Expr::Literal(Literal::Bool(true)))],
}));
assert_eq!(
text,
"let zd_enc_Pose (w: ZdWire.Writer) (v: Pose) : unit = true\n"
);
}
#[test]
fn the_module_line_always_carries_rec() {
let text = file(&File {
doc: Doc::none(),
module: Ident::new("ZdTypes"),
decls: Vec::new(),
});
assert!(text.starts_with("module rec ZdTypes\n"), "{text}");
}
#[test]
fn a_loop_break_renders_as_a_try_with_and_a_raise() {
let text = decl(&Decl::Let(LetDecl {
doc: Doc::none(),
name: Ident::new("f"),
params: Vec::new(),
type_: Some(Type::Primitive(Primitive::Unit)),
body: alloc::vec![
Stmt::Until {
condition: Expr::Literal(Literal::Bool(true)),
body: alloc::vec![Stmt::Leave],
},
Stmt::Return(Expr::Literal(Literal::Bool(true))),
],
}));
assert!(text.contains("try\n"), "{text}");
assert!(text.contains("while true do\n"), "{text}");
assert!(text.contains("raise (ZdWire.LoopBreak())\n"), "{text}");
assert!(text.contains(":? ZdWire.LoopBreak -> ()\n"), "{text}");
}
#[test]
fn a_counted_loop_writes_fs_for_syntax() {
let text = decl(&Decl::Let(LetDecl {
doc: Doc::none(),
name: Ident::new("f"),
params: Vec::new(),
type_: Some(Type::Primitive(Primitive::Unit)),
body: alloc::vec![
Stmt::CountUp {
counter: Ident::new("i"),
type_: Box::new(Type::Primitive(Primitive::UInt32)),
bound: Box::new(Expr::Name(Ident::new("n"))),
body: alloc::vec![Stmt::Expr(Expr::Name(Ident::new("i")))],
},
Stmt::Return(Expr::Literal(Literal::Bool(true))),
],
}));
assert!(text.contains("for i = 0u to (n) - 1u do\n"), "{text}");
}
#[test]
fn a_doc_comment_sits_above_the_field_it_belongs_to() {
let mut documented = field("type_", Type::Primitive(Primitive::Int32), Expr::int(0));
documented.doc = Doc::line("IDL: `type`.");
let text = decl(&class_decl(alloc::vec![documented]));
assert_eq!(
text,
"type Pose() =\n /// IDL: `type`.\n member val type_ : int32 = 0 with get, set\n"
);
}
}