use crate::writer::{Writer, comment_lines, verbatim_lines};
use alloc::string::{String, ToString};
use alloc::vec::Vec;
use zerodds_lang_cst::common::{BodyText, Doc, Ident, Literal};
use zerodds_lang_cst::zig::{
AnalyseAll, ConstDecl, Decl, Expr, Field, File, FuncDecl, Param, Stmt, Type, 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());
}
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 ident(name: &Ident) -> String {
name.as_str().to_string()
}
fn render_decl(w: &mut Writer, node: &Decl) {
match node {
Decl::Const(decl) => render_const(w, decl),
Decl::Func(decl) => render_func(w, decl),
Decl::Comment(lines) => comment_lines(w, "// ", lines.as_lines()),
Decl::Verbatim(text) => verbatim_lines(w, text),
Decl::AnalyseAll(block) => render_analyse_all(w, block),
}
}
fn render_func(w: &mut Writer, node: &FuncDecl) {
doc(w, &node.doc);
let mut head = String::new();
if node.is_pub {
head.push_str("pub ");
}
head.push_str("fn ");
head.push_str(node.name.as_str());
head.push('(');
let params: Vec<String> = node
.params
.iter()
.map(|param| {
let mut out = String::from(param.name.as_str());
out.push_str(": ");
out.push_str(&type_text(¶m.type_));
out
})
.collect();
head.push_str(¶ms.join(", "));
head.push_str(") ");
head.push_str(&type_text(&node.returns));
head.push_str(" {");
w.line(&head);
w.indent();
for statement in &node.body {
render_stmt(w, statement);
}
w.dedent();
w.line("}");
}
fn render_stmt(w: &mut Writer, node: &Stmt) {
match node {
Stmt::Var {
name,
type_,
value,
mutable,
} => {
let mut line = String::from(if *mutable { "var " } else { "const " });
line.push_str(name.as_str());
if let Some(type_) = type_ {
line.push_str(": ");
line.push_str(&type_text(type_));
}
line.push_str(" = ");
match value {
Some(value) => line.push_str(&expr_text(value)),
None => line.push_str("undefined"),
}
line.push(';');
w.line(&line);
}
Stmt::Assign { place, value } => {
let mut line = expr_text(place);
line.push_str(" = ");
line.push_str(&expr_text(value));
line.push(';');
w.line(&line);
}
Stmt::Expr(value) => {
let mut line = expr_text(value);
line.push(';');
w.line(&line);
}
Stmt::DiscardExpr(value) => {
let mut line = String::from("_ = ");
line.push_str(&expr_text(value));
line.push(';');
w.line(&line);
}
Stmt::Discard(name) => {
let mut line = String::from("_ = ");
line.push_str(name.as_str());
line.push(';');
w.line(&line);
}
Stmt::Return(value) => match value {
Some(value) => {
let mut line = String::from("return ");
line.push_str(&expr_text(value));
line.push(';');
w.line(&line);
}
None => w.line("return;"),
},
Stmt::If { .. } => render_if(w, node, "if ("),
Stmt::CountUp {
counter,
type_,
bound,
body,
} => {
w.line("{");
w.indent();
let mut line = String::from("var ");
line.push_str(counter.as_str());
line.push_str(": ");
line.push_str(&type_text(type_));
line.push_str(" = 0;");
w.line(&line);
let mut head = String::from("while (");
head.push_str(counter.as_str());
head.push_str(" < ");
head.push_str(&expr_text(bound));
head.push_str(") : (");
head.push_str(counter.as_str());
head.push_str(" += 1) {");
w.line(&head);
w.indent();
for statement in body {
render_stmt(w, statement);
}
w.dedent();
w.line("}");
w.dedent();
w.line("}");
}
Stmt::While { condition, body } => {
let mut head = String::from("while (");
head.push_str(&expr_text(condition));
head.push_str(") {");
w.line(&head);
w.indent();
for statement in body {
render_stmt(w, statement);
}
w.dedent();
w.line("}");
}
Stmt::Break => w.line("break;"),
Stmt::Comment(lines) => comment_lines(w, "// ", lines.as_lines()),
}
}
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(") {");
w.line(&head);
w.indent();
for statement in then {
render_stmt(w, statement);
}
w.dedent();
match otherwise {
None => {
w.line("}");
return;
}
Some(chain) if chain.len() == 1 && matches!(chain[0], Stmt::If { .. }) => {
node = &chain[0];
keyword = "} else if (";
}
Some(block) => {
w.line("} else {");
w.indent();
for statement in block {
render_stmt(w, statement);
}
w.dedent();
w.line("}");
return;
}
}
}
}
fn render_const(w: &mut Writer, node: &ConstDecl) {
doc(w, &node.doc);
let mut head = String::new();
if node.is_pub {
head.push_str("pub ");
}
head.push_str("const ");
head.push_str(node.name.as_str());
if let Some(type_) = &node.type_ {
head.push_str(": ");
head.push_str(&type_text(type_));
}
head.push_str(" = ");
match &node.value {
Expr::Type(type_) => render_container(w, head, type_, &node.body_text),
other => {
head.push_str(&expr_text(other));
head.push(';');
w.line(&head);
}
}
}
fn render_container(w: &mut Writer, mut head: String, node: &Type, body_text: &BodyText) {
let has_text = !body_text.begin.is_empty() || !body_text.end.is_empty();
match node {
Type::Struct(fields) if !fields.is_empty() || has_text => {
head.push_str("struct {");
w.line(&head);
w.indent();
for text in &body_text.begin {
verbatim_lines(w, text);
}
for field in fields {
render_field(w, field);
}
for text in &body_text.end {
verbatim_lines(w, text);
}
w.dedent();
w.line("};");
}
Type::Enum { tag, variants } if !variants.is_empty() => {
head.push_str("enum(");
head.push_str(&type_text(tag));
head.push_str(") {");
w.line(&head);
w.indent();
for variant in variants {
render_variant(w, variant);
}
w.dedent();
w.line("};");
}
other => {
head.push_str(&type_text(other));
head.push(';');
w.line(&head);
}
}
}
fn render_field(w: &mut Writer, node: &Field) {
doc(w, &node.doc);
let mut line = String::from(node.name.as_str());
line.push_str(": ");
line.push_str(&type_text(&node.type_));
if let Some(default) = &node.default {
line.push_str(" = ");
line.push_str(&expr_text(default));
}
line.push(',');
w.line(&line);
}
fn render_variant(w: &mut Writer, node: &Variant) {
doc(w, &node.doc);
let mut line = String::from(node.name.as_str());
if let Some(value) = &node.value {
line.push_str(" = ");
line.push_str(&expr_text(value));
}
line.push(',');
w.line(&line);
}
fn render_analyse_all(w: &mut Writer, node: &AnalyseAll) {
if !node.doc.is_empty() {
comment_lines(w, "// ", node.doc.as_lines());
}
w.line("test {");
w.indent();
w.line("const zd_walk = struct {");
w.indent();
w.line("fn all(comptime zd_T: type) void {");
w.indent();
w.line("inline for (comptime @import(\"std\").meta.declarations(zd_T)) |zd_decl| {");
w.indent();
w.line("const zd_field = @field(zd_T, zd_decl.name);");
w.line("if (@TypeOf(zd_field) == type) {");
w.indent();
w.line("switch (@typeInfo(zd_field)) {");
w.indent();
w.line(".@\"struct\", .@\"enum\", .@\"union\", .@\"opaque\" => all(zd_field),");
w.line("else => {},");
w.dedent();
w.line("}");
w.dedent();
w.line("}");
w.line("_ = &zd_field;");
w.dedent();
w.line("}");
w.dedent();
w.line("}");
w.dedent();
w.line("}.all;");
w.line("zd_walk(@This());");
w.dedent();
w.line("}");
}
fn container_text(node: &Type) -> String {
let mut w = Writer::spaces4();
render_container(&mut w, String::new(), node, &BodyText::none());
w.finish().trim_end().trim_end_matches(';').to_string()
}
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::Slice { is_const, element } => {
let mut out = String::from("[]");
if *is_const {
out.push_str("const ");
}
out.push_str(&type_text(element));
out
}
Type::Array { length, element } => {
let mut out = String::from("[");
out.push_str(&expr_text(length));
out.push(']');
out.push_str(&type_text(element));
out
}
Type::Optional(inner) => {
let mut out = String::from("?");
out.push_str(&type_text(inner));
out
}
Type::Pointer { is_const, target } => {
let mut out = String::from("*");
if *is_const {
out.push_str("const ");
}
out.push_str(&type_text(target));
out
}
Type::Struct(fields) => {
if fields.is_empty() {
return "struct {}".to_string();
}
container_text(node)
}
Type::Enum { tag, variants } => {
if variants.is_empty() {
let mut out = String::from("enum(");
out.push_str(&type_text(tag));
out.push_str(") {}");
return out;
}
container_text(node)
}
Type::Fn { params, returns } => {
let mut out = String::from("fn (");
let parts: Vec<String> = params.iter().map(param_text).collect();
out.push_str(&parts.join(", "));
out.push_str(") ");
out.push_str(&type_text(returns));
out
}
Type::ErrorUnion { error_set, payload } => {
let mut out = type_text(error_set);
out.push('!');
out.push_str(&type_text(payload));
out
}
}
}
fn param_text(param: &Param) -> String {
let mut out = String::new();
if let Some(name) = ¶m.name {
out.push_str(name.as_str());
out.push_str(": ");
}
out.push_str(&type_text(¶m.type_));
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::Type(type_) => type_text(type_),
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::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('[');
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::Try(value) => {
let mut out = String::from("try ");
out.push_str(&expr_text(value));
out
}
Expr::StructLit(fields) => {
if fields.is_empty() {
return ".{}".to_string();
}
let mut out = String::from(".{ ");
let parts: Vec<String> = fields
.iter()
.map(|(name, value)| {
let mut part = String::from(".");
part.push_str(name.as_str());
part.push_str(" = ");
part.push_str(&expr_text(value));
part
})
.collect();
out.push_str(&parts.join(", "));
out.push_str(" }");
out
}
Expr::ListLit(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
}
}
}
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) => {
let mut out = String::from("'");
out.push_str(&char_text(*ch));
out.push('\'');
out
}
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("&[_]u8{ ");
let parts: Vec<String> = bytes.iter().map(|b| alloc::format!("0x{b:02x}")).collect();
out.push_str(&parts.join(", "));
out.push_str(" }");
out
}
Literal::Null => "null".to_string(),
}
}
#[must_use]
pub fn string_text(text: &str) -> String {
let mut out = String::new();
for ch in text.chars() {
push_escaped(&mut out, ch, '"');
}
out
}
#[must_use]
pub fn char_text(ch: char) -> String {
let mut out = String::new();
push_escaped(&mut out, ch, '\'');
out
}
fn push_escaped(out: &mut String, ch: char, delimiter: char) {
match ch {
'\\' => out.push_str("\\\\"),
'\n' => out.push_str("\\n"),
'\r' => out.push_str("\\r"),
'\t' => out.push_str("\\t"),
c if c == delimiter => {
out.push('\\');
out.push(c);
}
c if (c as u32) < 0x20 || c as u32 == 0x7F => {
out.push_str(&alloc::format!("\\x{:02x}", c as u32));
}
other => out.push(other),
}
}
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 zerodds_lang_cst::zig::Primitive;
fn struct_decl(fields: Vec<Field>) -> Decl {
Decl::Const(ConstDecl {
body_text: zerodds_lang_cst::common::BodyText::none(),
doc: Doc::none(),
is_pub: true,
name: Ident::new("S"),
type_: None,
value: Expr::type_(Type::Struct(fields)),
})
}
fn field(name: &str, type_: Type) -> Field {
Field {
doc: Doc::none(),
name: Ident::new(name),
type_,
default: None,
}
}
#[test]
fn a_struct_declaration_indents_by_four_and_ends_every_field_in_a_comma() {
let text = decl(&struct_decl(alloc::vec![
field("x", Type::Primitive(Primitive::I32)),
field("y", Type::Primitive(Primitive::F64)),
]));
assert_eq!(
text,
"pub const S = struct {\n x: i32,\n y: f64,\n};\n"
);
}
#[test]
fn an_empty_struct_stays_on_one_line() {
assert_eq!(decl(&struct_decl(Vec::new())), "pub const S = struct {};\n");
}
#[test]
fn a_type_reads_left_to_right_with_no_declarator_to_split() {
assert_eq!(
type_(&Type::array(
2,
Type::array(3, Type::Primitive(Primitive::I32))
)),
"[2][3]i32"
);
assert_eq!(type_(&Type::text()), "[]const u8");
assert_eq!(type_(&Type::slice(Type::named("Entry"))), "[]Entry");
assert_eq!(
type_(&Type::optional(Type::Primitive(Primitive::U8))),
"?u8"
);
assert_eq!(
type_(&Type::pointer(Type::Primitive(Primitive::AnyOpaque))),
"*anyopaque"
);
}
#[test]
fn a_function_pointer_carries_its_parameter_names() {
let type_ = Type::const_pointer(Type::Fn {
params: alloc::vec![Param {
name: Some(Ident::new("target")),
type_: Type::named("Pose"),
}],
returns: alloc::boxed::Box::new(Type::Primitive(Primitive::Void)),
});
assert_eq!(super::type_(&type_), "*const fn (target: Pose) void");
}
#[test]
fn an_optional_field_writes_its_absence_as_the_default() {
let mut optional = field("b", Type::optional(Type::Primitive(Primitive::U8)));
optional.default = Some(Expr::Literal(Literal::Null));
let text = decl(&struct_decl(alloc::vec![optional]));
assert!(text.contains("b: ?u8 = null,"), "{text}");
}
#[test]
fn an_enum_writes_its_tag_type_and_every_value() {
let text = decl(&Decl::Const(ConstDecl {
body_text: zerodds_lang_cst::common::BodyText::none(),
doc: Doc::none(),
is_pub: true,
name: Ident::new("Mode"),
type_: None,
value: Expr::type_(Type::Enum {
tag: alloc::boxed::Box::new(Type::Primitive(Primitive::I32)),
variants: alloc::vec![
Variant {
doc: Doc::none(),
name: Ident::new("IDLE"),
value: Some(Expr::int(0)),
},
Variant {
doc: Doc::none(),
name: Ident::new("ACTIVE"),
value: Some(Expr::int(4)),
},
],
}),
}));
assert_eq!(
text,
"pub const Mode = enum(i32) {\n IDLE = 0,\n ACTIVE = 4,\n};\n"
);
}
#[test]
fn a_constant_writes_its_ascription_before_its_value() {
let text = decl(&Decl::Const(ConstDecl {
body_text: zerodds_lang_cst::common::BodyText::none(),
doc: Doc::none(),
is_pub: true,
name: Ident::new("READY"),
type_: Some(Type::named("Flags")),
value: Expr::Binary {
op: "<<",
lhs: alloc::boxed::Box::new(Expr::int(1)),
rhs: alloc::boxed::Box::new(Expr::int(4)),
},
}));
assert_eq!(text, "pub const READY: Flags = 1 << 4;\n");
}
#[test]
fn a_nul_is_written_as_a_hex_escape_because_zig_has_no_backslash_zero() {
assert_eq!(string_text("a\0b"), "a\\x00b");
assert_eq!(string_text("a\"b\\c"), "a\\\"b\\\\c");
assert_eq!(string_text("line\nnext\ttab"), "line\\nnext\\ttab");
assert_eq!(string_text("hä"), "hä");
}
#[test]
fn the_file_comment_is_the_container_form_and_the_test_block_has_none() {
let text = file(&File {
doc: Doc::lines(&["Generated.", "", "Do not edit."]),
decls: alloc::vec![Decl::AnalyseAll(AnalyseAll {
doc: Doc::line("Why this block is here."),
})],
});
assert!(
text.starts_with("//! Generated.\n//!\n//! Do not edit.\n"),
"{text}"
);
assert!(text.contains("// Why this block is here.\n"), "{text}");
assert!(text.contains("test {\n"), "{text}");
assert!(
!text.contains("/// Why"),
"a documentation comment was attached to a test block\n{text}"
);
}
#[test]
fn a_doc_comment_sits_above_the_field_it_belongs_to() {
let mut documented = field("type_", Type::Primitive(Primitive::I32));
documented.doc = Doc::line("IDL: `type`.");
let text = decl(&struct_decl(alloc::vec![documented]));
assert_eq!(
text,
"pub const S = struct {\n /// IDL: `type`.\n type_: i32,\n};\n"
);
}
}