Skip to main content

zerodds_lang_render/
d.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2026 ZeroDDS Contributors
3//! Rendering the D CST. Spec §5.
4//!
5//! One style, the one `dfmt` and Phobos use: four spaces, Allman braces
6//! — the opening brace on a line of its own — and `///` for
7//! documentation, which is what Ddoc reads.
8//!
9//! Nothing is decided here, with one exception that is a rendering fact
10//! rather than a choice: **a D type suffix is written after the type it
11//! applies to and reads right to left**. `int[3][2]` is two arrays of
12//! three (D language specification, "Arrays", "Rectangular Arrays"),
13//! which is the reverse of C. [`zerodds_lang_cst::d::Type::Array`] holds
14//! the outermost dimension outermost, exactly as the Go tree does, and
15//! this renderer writes the element before the length — so one tree
16//! shape produces `[2][3]int32` in Go and `int[3][2]` in D and both mean
17//! two rows of three.
18//!
19//! Unlike [`crate::c`] there is no declarator to split around
20//! a name: a D declaration is the whole type, a space, and the name.
21
22use crate::writer::{Writer, comment_lines, escape_string, verbatim_lines};
23use alloc::string::{String, ToString};
24use alloc::vec::Vec;
25use zerodds_lang_cst::common::{Doc, Ident, Literal, Path};
26use zerodds_lang_cst::d::{
27    AliasDecl, ConstDecl, Decl, EnumDecl, EnumMember, Expr, Field, File, FuncDecl, Import,
28    InterfaceDecl, Member, Method, Param, Stmt, StructDecl, Type, Uda,
29};
30
31/// Render a whole file.
32#[must_use]
33pub fn file(file: &File) -> String {
34    let mut w = Writer::spaces4();
35    doc(&mut w, &file.doc);
36    let mut head = String::from("module ");
37    head.push_str(&path_text(&file.module));
38    head.push(';');
39    w.line(&head);
40    if !file.imports.is_empty() {
41        w.blank_line();
42        for import in &file.imports {
43            w.line(&import_text(import));
44        }
45    }
46    for decl in &file.decls {
47        w.blank_line();
48        render_decl(&mut w, decl);
49    }
50    w.finish()
51}
52
53/// Render one declaration, for a caller that has only one.
54#[must_use]
55pub fn decl(node: &Decl) -> String {
56    let mut w = Writer::spaces4();
57    render_decl(&mut w, node);
58    w.finish()
59}
60
61/// Render one type, for a caller that has only one.
62#[must_use]
63pub fn type_(node: &Type) -> String {
64    type_text(node)
65}
66
67/// The identifier a name renders as, for a caller assembling one.
68#[must_use]
69pub fn ident(name: &Ident) -> String {
70    name.as_str().to_string()
71}
72
73/// A dotted module path: `std.typecons`.
74fn path_text(path: &Path) -> String {
75    path.segments()
76        .iter()
77        .map(|segment| segment.as_str())
78        .collect::<Vec<&str>>()
79        .join(".")
80}
81
82fn import_text(import: &Import) -> String {
83    let mut out = String::from("import ");
84    out.push_str(&path_text(&import.path));
85    if !import.symbols.is_empty() {
86        out.push_str(" : ");
87        out.push_str(
88            &import
89                .symbols
90                .iter()
91                .map(Ident::as_str)
92                .collect::<Vec<&str>>()
93                .join(", "),
94        );
95    }
96    out.push(';');
97    out
98}
99
100/// `ReturnType name(params)` with an Allman body.
101fn render_func(w: &mut Writer, node: &FuncDecl) {
102    doc(w, &node.doc);
103    let mut head = String::new();
104    match &node.returns {
105        Some(returns) => head.push_str(&type_text(returns)),
106        None => head.push_str("void"),
107    }
108    head.push(' ');
109    head.push_str(node.name.as_str());
110    head.push('(');
111    let params: Vec<String> = node.params.iter().map(param_text).collect();
112    head.push_str(&params.join(", "));
113    head.push(')');
114    w.line(&head);
115    w.line("{");
116    w.indent();
117    render_stmts(w, &node.body);
118    w.dedent();
119    w.line("}");
120}
121
122/// zerodds-lint: recursion-depth 64 (one level per nested block of a
123/// generated body. The codec walk emits a routine per type rather than
124/// inlining one — `lang-rules/src/emit/encode.rs` — so a body nests by
125/// its own framing and by one loop per array dimension, never by the
126/// type graph, whose descent stops at 64 anyway; the else-if chain, one
127/// arm per union case, is walked iteratively and costs no depth.)
128fn render_stmts(w: &mut Writer, body: &[Stmt]) {
129    for statement in body {
130        render_stmt(w, statement);
131    }
132}
133
134/// zerodds-lint: recursion-depth 64 (one level per nested block of a
135/// generated body. The codec walk emits a routine per type rather than
136/// inlining one — `lang-rules/src/emit/encode.rs` — so a body nests by
137/// its own framing and by one loop per array dimension, never by the
138/// type graph, whose descent stops at 64 anyway; the else-if chain, one
139/// arm per union case, is walked iteratively and costs no depth.)
140fn render_stmt(w: &mut Writer, node: &Stmt) {
141    match node {
142        Stmt::Var { name, type_, value } => {
143            let mut line;
144            match (type_, value) {
145                // `auto name = value;`: the type is the initialiser's.
146                (None, Some(value)) => {
147                    line = String::from("auto ");
148                    line.push_str(name.as_str());
149                    line.push_str(" = ");
150                    line.push_str(&expr_text(value));
151                }
152                (Some(type_), value) => {
153                    line = type_text(type_);
154                    line.push(' ');
155                    line.push_str(name.as_str());
156                    if let Some(value) = value {
157                        line.push_str(" = ");
158                        line.push_str(&expr_text(value));
159                    }
160                }
161                // Neither a type nor a value is not a declaration;
162                // written out so the defect names itself in the diff.
163                (None, None) => {
164                    line = String::from("auto ");
165                    line.push_str(name.as_str());
166                }
167            }
168            line.push(';');
169            w.line(&line);
170        }
171        Stmt::Assign { place, value } => {
172            let mut line = expr_text(place);
173            line.push_str(" = ");
174            line.push_str(&expr_text(value));
175            line.push(';');
176            w.line(&line);
177        }
178        Stmt::Expr(value) => {
179            let mut line = expr_text(value);
180            line.push(';');
181            w.line(&line);
182        }
183        Stmt::Return(value) => match value {
184            Some(value) => {
185                let mut line = String::from("return ");
186                line.push_str(&expr_text(value));
187                line.push(';');
188                w.line(&line);
189            }
190            None => w.line("return;"),
191        },
192        Stmt::If {
193            condition,
194            then,
195            otherwise,
196        } => render_if(w, condition, then, otherwise.as_deref(), "if ("),
197        Stmt::For {
198            counter,
199            type_,
200            bound,
201            body,
202        } => {
203            let mut line = String::from("for (");
204            line.push_str(&type_text(type_));
205            line.push(' ');
206            line.push_str(counter.as_str());
207            line.push_str(" = 0; ");
208            line.push_str(counter.as_str());
209            line.push_str(" < ");
210            line.push_str(&expr_text(bound));
211            line.push_str("; ");
212            line.push_str(counter.as_str());
213            line.push_str("++)");
214            w.line(&line);
215            w.line("{");
216            w.indent();
217            render_stmts(w, body);
218            w.dedent();
219            w.line("}");
220        }
221        Stmt::While { condition, body } => {
222            let mut line = String::from("while (");
223            line.push_str(&expr_text(condition));
224            line.push(')');
225            w.line(&line);
226            w.line("{");
227            w.indent();
228            render_stmts(w, body);
229            w.dedent();
230            w.line("}");
231        }
232        Stmt::Break => w.line("break;"),
233        Stmt::Comment(lines) => doc(w, lines),
234    }
235}
236
237/// An `if` chain, with `else if` continuing the line the way dfmt
238/// writes it: an else-branch holding exactly one `if` and nothing else
239/// does not nest.
240///
241/// The next arm is taken in a loop and not by a call to this function
242/// again. The chain's length is the union's case count, the
243/// enumeration's enumerator count or the `@mutable` aggregate's member
244/// count — `lang-rules/src/emit/encode.rs` builds one `If` per arm, each
245/// in the previous one's `else` — and nothing in the model bounds any of
246/// the three.
247///
248/// zerodds-lint: recursion-depth 64 (block nesting only; the arm chain
249/// is walked iteratively, and what remains is one level per nested block
250/// of a generated body — the codec walk emits a routine per type rather
251/// than inlining, so a body nests by its framing and by one loop per
252/// array dimension, never by the type graph)
253fn render_if(
254    w: &mut Writer,
255    condition: &Expr,
256    then: &[Stmt],
257    otherwise: Option<&[Stmt]>,
258    head: &str,
259) {
260    let mut condition = condition;
261    let mut then = then;
262    let mut otherwise = otherwise;
263    let mut head = head;
264    loop {
265        let mut line = String::from(head);
266        line.push_str(&expr_text(condition));
267        line.push(')');
268        w.line(&line);
269        w.line("{");
270        w.indent();
271        render_stmts(w, then);
272        w.dedent();
273        w.line("}");
274        match otherwise {
275            None => return,
276            Some(
277                [
278                    Stmt::If {
279                        condition: next_condition,
280                        then: next_then,
281                        otherwise: next_otherwise,
282                    },
283                ],
284            ) => {
285                condition = next_condition;
286                then = next_then;
287                otherwise = next_otherwise.as_deref();
288                head = "else if (";
289            }
290            Some(branch) => {
291                w.line("else");
292                w.line("{");
293                w.indent();
294                render_stmts(w, branch);
295                w.dedent();
296                w.line("}");
297                return;
298            }
299        }
300    }
301}
302
303fn render_decl(w: &mut Writer, node: &Decl) {
304    match node {
305        Decl::Struct(decl) => render_struct(w, decl),
306        Decl::Enum(decl) => render_enum(w, decl),
307        Decl::Alias(decl) => render_alias(w, decl),
308        Decl::Const(decl) => render_const(w, decl),
309        Decl::Interface(decl) => render_interface(w, decl),
310        Decl::Func(decl) => render_func(w, decl),
311        Decl::Comment(lines) => doc(w, lines),
312        Decl::Verbatim(text) => verbatim_lines(w, text),
313    }
314}
315
316fn render_struct(w: &mut Writer, node: &StructDecl) {
317    doc(w, &node.doc);
318    let mut head = String::from("struct ");
319    head.push_str(node.name.as_str());
320    w.line(&head);
321    w.line("{");
322    w.indent();
323    for member in &node.members {
324        render_member(w, member);
325    }
326    w.dedent();
327    w.line("}");
328}
329
330fn render_member(w: &mut Writer, node: &Member) {
331    match node {
332        Member::Field(field) => render_field(w, field),
333        Member::Verbatim(text) => verbatim_lines(w, text),
334        Member::Union(fields) => {
335            // An empty `union { }` is never written: it says nothing and
336            // is a body a reader has to check the grammar for.
337            if fields.is_empty() {
338                return;
339            }
340            w.line("union");
341            w.line("{");
342            w.indent();
343            for field in fields {
344                render_field(w, field);
345            }
346            w.dedent();
347            w.line("}");
348        }
349    }
350}
351
352fn render_field(w: &mut Writer, node: &Field) {
353    doc(w, &node.doc);
354    let mut line = String::new();
355    for uda in &node.udas {
356        line.push_str(&uda_text(uda));
357        line.push(' ');
358    }
359    line.push_str(&type_text(&node.type_));
360    line.push(' ');
361    line.push_str(node.name.as_str());
362    line.push(';');
363    w.line(&line);
364}
365
366/// A user-defined attribute: `@("idl:x")`.
367///
368/// A single string, parenthesised. `@(…)` is the general form
369/// (D language specification, "Attributes", "User-Defined Attributes");
370/// the bare `@name` form names a symbol, which would need a declaration
371/// this file would then have to emit and every consumer import.
372fn uda_text(uda: &Uda) -> String {
373    let mut out = String::from("@(\"");
374    out.push_str(&escape_string(&uda.text));
375    out.push_str("\")");
376    out
377}
378
379fn render_enum(w: &mut Writer, node: &EnumDecl) {
380    doc(w, &node.doc);
381    let mut head = String::from("enum ");
382    head.push_str(node.name.as_str());
383    if let Some(base) = &node.base {
384        head.push_str(" : ");
385        head.push_str(&type_text(base));
386    }
387    w.line(&head);
388    w.line("{");
389    w.indent();
390    for (index, member) in node.members.iter().enumerate() {
391        let last = index + 1 == node.members.len();
392        render_enum_member(w, member, last);
393    }
394    w.dedent();
395    w.line("}");
396}
397
398/// One enum member, with the comma every one but the last carries.
399///
400/// No trailing comma on the last: D accepts one, and leaving it off
401/// keeps the output the same shape as the enum bodies in Phobos.
402fn render_enum_member(w: &mut Writer, node: &EnumMember, last: bool) {
403    doc(w, &node.doc);
404    let mut line = String::from(node.name.as_str());
405    line.push_str(" = ");
406    line.push_str(&expr_text(&node.value));
407    if !last {
408        line.push(',');
409    }
410    w.line(&line);
411}
412
413fn render_alias(w: &mut Writer, node: &AliasDecl) {
414    doc(w, &node.doc);
415    // `alias Name = T;` and not the old `alias T Name;`. The second form
416    // is deprecated and reads in the opposite order, which is what made
417    // it deprecated.
418    let mut line = String::from("alias ");
419    line.push_str(node.name.as_str());
420    line.push_str(" = ");
421    line.push_str(&type_text(&node.type_));
422    line.push(';');
423    w.line(&line);
424}
425
426/// A manifest constant: `enum T NAME = value;`.
427///
428/// `enum` and not `immutable`: a manifest constant has no storage and no
429/// address (D language specification, "Enums", "Manifest Constants"),
430/// where an `immutable` declaration at module scope is a variable with
431/// both. A constant that a consumer can take the address of is a symbol
432/// the linker has to place, per generated file, in every binary that
433/// imports it.
434fn render_const(w: &mut Writer, node: &ConstDecl) {
435    doc(w, &node.doc);
436    let mut line = String::from("enum ");
437    if let Some(type_) = &node.type_ {
438        line.push_str(&type_text(type_));
439        line.push(' ');
440    }
441    line.push_str(node.name.as_str());
442    line.push_str(" = ");
443    line.push_str(&expr_text(&node.value));
444    line.push(';');
445    w.line(&line);
446}
447
448fn render_interface(w: &mut Writer, node: &InterfaceDecl) {
449    doc(w, &node.doc);
450    let mut head = String::from("interface ");
451    head.push_str(node.name.as_str());
452    w.line(&head);
453    w.line("{");
454    w.indent();
455    for method in &node.methods {
456        render_method(w, method);
457    }
458    w.dedent();
459    w.line("}");
460}
461
462fn render_method(w: &mut Writer, node: &Method) {
463    doc(w, &node.doc);
464    let mut line = match &node.returns {
465        Some(type_) => type_text(type_),
466        None => "void".to_string(),
467    };
468    line.push(' ');
469    line.push_str(node.name.as_str());
470    line.push('(');
471    let params: Vec<String> = node.params.iter().map(param_text).collect();
472    line.push_str(&params.join(", "));
473    line.push_str(");");
474    w.line(&line);
475}
476
477fn param_text(param: &Param) -> String {
478    let mut out = type_text(&param.type_);
479    out.push(' ');
480    out.push_str(param.name.as_str());
481    out
482}
483
484/// A type, with every suffix written after what it applies to.
485///
486/// The direction is the whole of what this function decides, and it is
487/// D's: `int[3][2]` is two arrays of three. A tree holding the outermost
488/// dimension outermost therefore renders its element first and appends
489/// its own length last.
490///
491/// zerodds-lint: recursion-depth 64 (one level per collection, alias or
492/// generic argument of the spelled type; `MAX_TYPE_DEPTH` in
493/// `lang-rules/src/emit/d.rs` refuses a type that nests deeper)
494fn type_text(node: &Type) -> String {
495    match node {
496        Type::Primitive(primitive) => primitive.as_str().to_string(),
497        Type::Named(name) => name.as_str().to_string(),
498        Type::Slice(element) => {
499            let mut out = type_text(element);
500            out.push_str("[]");
501            out
502        }
503        Type::Array { length, element } => {
504            let mut out = type_text(element);
505            out.push('[');
506            out.push_str(&expr_text(length));
507            out.push(']');
508            out
509        }
510        Type::Pointer(target) => {
511            let mut out = type_text(target);
512            out.push('*');
513            out
514        }
515        // Always parenthesised: `Nullable!int[]` parses as
516        // `(Nullable!int)[]`, because D's single-token template argument
517        // binds to the token and not to the suffix after it.
518        Type::Nullable(target) => {
519            let mut out = String::from("Nullable!(");
520            out.push_str(&type_text(target));
521            out.push(')');
522            out
523        }
524    }
525}
526
527/// zerodds-lint: recursion-depth = the built expression's nesting. A walk
528/// calls a routine per type rather than nesting one, so the one operand
529/// tree that grows with the IDL is an array's default value: one composite
530/// literal per dimension, built in `lang-rules/src/default_d.rs`, and
531/// nothing bounds the dimension count. No number is claimed: this
532/// signature returns no error.
533fn expr_text(node: &Expr) -> String {
534    match node {
535        Expr::Literal(literal) => literal_text(literal),
536        Expr::Name(name) => name.as_str().to_string(),
537        Expr::Binary { op, lhs, rhs } => {
538            let mut out = expr_text(lhs);
539            out.push(' ');
540            out.push_str(op);
541            out.push(' ');
542            out.push_str(&expr_text(rhs));
543            out
544        }
545        Expr::Cast { type_, value } => {
546            let mut out = String::from("cast(");
547            out.push_str(&type_text(type_));
548            out.push(')');
549            out.push_str(&expr_text(value));
550            out
551        }
552        Expr::Paren(inner) => {
553            let mut out = String::from("(");
554            out.push_str(&expr_text(inner));
555            out.push(')');
556            out
557        }
558        Expr::Call { func, args } => {
559            let mut out = expr_text(func);
560            out.push('(');
561            let parts: Vec<String> = args.iter().map(expr_text).collect();
562            out.push_str(&parts.join(", "));
563            out.push(')');
564            out
565        }
566        Expr::Dot { value, name } => {
567            let mut out = expr_text(value);
568            out.push('.');
569            out.push_str(name.as_str());
570            out
571        }
572        Expr::Index { value, index } => {
573            let mut out = expr_text(value);
574            out.push('[');
575            out.push_str(&expr_text(index));
576            out.push(']');
577            out
578        }
579        Expr::SliceTo { value, high } => {
580            let mut out = expr_text(value);
581            out.push_str("[0 .. ");
582            out.push_str(&expr_text(high));
583            out.push(']');
584            out
585        }
586        Expr::Unary { op, value } => {
587            let mut out = String::from(*op);
588            out.push_str(&expr_text(value));
589            out
590        }
591        Expr::StructLit { name, fields } => {
592            let mut out = String::from(name.as_str());
593            out.push('(');
594            let parts: Vec<String> = fields
595                .iter()
596                .map(|(key, value)| match key {
597                    Some(key) => {
598                        let mut part = String::from(key.as_str());
599                        part.push_str(": ");
600                        part.push_str(&expr_text(value));
601                        part
602                    }
603                    None => expr_text(value),
604                })
605                .collect();
606            out.push_str(&parts.join(", "));
607            out.push(')');
608            out
609        }
610        Expr::ArrayLit(elements) => {
611            let mut out = String::from("[");
612            let parts: Vec<String> = elements.iter().map(expr_text).collect();
613            out.push_str(&parts.join(", "));
614            out.push(']');
615            out
616        }
617        Expr::NewSlice { element, length } => {
618            let mut out = String::from("new ");
619            out.push_str(&type_text(element));
620            out.push_str("[](");
621            out.push_str(&expr_text(length));
622            out.push(')');
623            out
624        }
625    }
626}
627
628fn literal_text(literal: &Literal) -> String {
629    match literal {
630        Literal::Int(text) | Literal::Float(text) => text.clone(),
631        Literal::Bool(value) => {
632            if *value {
633                "true".to_string()
634            } else {
635                "false".to_string()
636            }
637        }
638        // The code point as a number and not `'c'`. A D character
639        // literal has type `char`, `wchar` or `dchar` — UTF code units —
640        // and an IDL `char` is one octet held here in a `ubyte`. Writing
641        // the number puts the conversion at the declaration, where the
642        // declared type states it, rather than inside a literal that
643        // happens to fit.
644        Literal::Char(ch) => alloc::format!("{}", u32::from(*ch)),
645        Literal::Str(text) => {
646            let mut out = String::from("\"");
647            out.push_str(&escape_string(text));
648            out.push('"');
649            out
650        }
651        // A D array literal of unsigned octets. `cast(ubyte[])` is not
652        // written: the declared type of what this initialises says
653        // `ubyte[]`, and the literal's elements are all in range.
654        Literal::Bytes(bytes) => {
655            let mut out = String::from("[");
656            let parts: Vec<String> = bytes.iter().map(|b| alloc::format!("0x{b:02X}")).collect();
657            out.push_str(&parts.join(", "));
658            out.push(']');
659            out
660        }
661        Literal::Null => "null".to_string(),
662    }
663}
664
665fn doc(w: &mut Writer, doc: &Doc) {
666    if doc.is_empty() {
667        return;
668    }
669    comment_lines(w, "/// ", doc.as_lines());
670}
671
672#[cfg(test)]
673mod tests {
674    #![allow(clippy::expect_used, clippy::panic, clippy::unwrap_used)]
675    use super::*;
676    use alloc::vec;
677    use zerodds_lang_cst::d::Primitive;
678
679    fn field(name: &str, type_: Type) -> Field {
680        Field {
681            doc: Doc::none(),
682            udas: Vec::new(),
683            name: Ident::new(name),
684            type_,
685        }
686    }
687
688    #[test]
689    fn a_file_declares_its_module_first() {
690        let rendered = file(&File::default());
691        assert_eq!(rendered, "module zdtypes;\n");
692    }
693
694    #[test]
695    fn an_import_is_selective_when_it_names_symbols() {
696        let mut source = File::default();
697        source
698            .imports
699            .push(Import::selective(&["std", "typecons"], "Nullable"));
700        let rendered = file(&source);
701        assert!(
702            rendered.contains("import std.typecons : Nullable;"),
703            "{rendered}"
704        );
705    }
706
707    #[test]
708    fn a_struct_writes_its_brace_on_a_line_of_its_own() {
709        let rendered = decl(&Decl::Struct(StructDecl {
710            doc: Doc::none(),
711            name: Ident::new("Pose"),
712            members: vec![Member::Field(field("x", Type::Primitive(Primitive::Int)))],
713        }));
714        assert_eq!(rendered, "struct Pose\n{\n    int x;\n}\n");
715    }
716
717    #[test]
718    fn a_field_carries_its_uda_before_the_type() {
719        let mut node = field("version_", Type::Primitive(Primitive::Int));
720        node.udas.push(Uda::keyed("idl", "version"));
721        let mut w = Writer::spaces4();
722        render_field(&mut w, &node);
723        assert_eq!(w.finish(), "@(\"idl:version\") int version_;\n");
724    }
725
726    #[test]
727    fn an_array_writes_its_dimensions_right_to_left() {
728        // `long grid[2][3]` is two rows of three; the D declaration is
729        // `int[3][2]`. The tree holds 2 outermost and the renderer
730        // appends it last, which is what produces the reversal.
731        let type_ = Type::array(2, Type::array(3, Type::Primitive(Primitive::Int)));
732        assert_eq!(type_text(&type_), "int[3][2]");
733    }
734
735    #[test]
736    fn a_slice_of_a_static_array_keeps_the_two_apart() {
737        // `sequence<long[3]>` is a dynamic array whose element is
738        // `int[3]`, which D writes `int[3][]`.
739        let type_ = Type::slice(Type::array(3, Type::Primitive(Primitive::Int)));
740        assert_eq!(type_text(&type_), "int[3][]");
741        // And the other way round: a static array of two slices.
742        let other = Type::array(2, Type::slice(Type::Primitive(Primitive::Int)));
743        assert_eq!(type_text(&other), "int[][2]");
744    }
745
746    #[test]
747    fn a_nullable_parenthesises_its_argument() {
748        // `Nullable!int[]` parses as `(Nullable!int)[]`. The
749        // parenthesised form is the only one that cannot be read the
750        // wrong way.
751        let type_ = Type::nullable(Type::slice(Type::Primitive(Primitive::Int)));
752        assert_eq!(type_text(&type_), "Nullable!(int[])");
753    }
754
755    #[test]
756    fn a_pointer_writes_its_star_after_the_type() {
757        assert_eq!(
758            type_text(&Type::pointer(Type::Primitive(Primitive::Void))),
759            "void*"
760        );
761    }
762
763    #[test]
764    fn an_enum_states_its_base_and_every_value() {
765        let rendered = decl(&Decl::Enum(EnumDecl {
766            doc: Doc::none(),
767            name: Ident::new("Mode"),
768            base: Some(Type::Primitive(Primitive::Int)),
769            members: vec![
770                EnumMember {
771                    doc: Doc::none(),
772                    name: Ident::new("IDLE"),
773                    value: Expr::int(0),
774                },
775                EnumMember {
776                    doc: Doc::none(),
777                    name: Ident::new("ACTIVE"),
778                    value: Expr::int(4),
779                },
780            ],
781        }));
782        assert_eq!(
783            rendered,
784            "enum Mode : int\n{\n    IDLE = 0,\n    ACTIVE = 4\n}\n"
785        );
786    }
787
788    #[test]
789    fn a_manifest_constant_is_an_enum_and_not_an_immutable() {
790        let rendered = decl(&Decl::Const(ConstDecl {
791            doc: Doc::none(),
792            name: Ident::new("LIMIT"),
793            type_: Some(Type::Primitive(Primitive::UByte)),
794            value: Expr::int(8),
795        }));
796        assert_eq!(rendered, "enum ubyte LIMIT = 8;\n");
797    }
798
799    #[test]
800    fn an_alias_is_written_in_the_new_order() {
801        let rendered = decl(&Decl::Alias(AliasDecl {
802            doc: Doc::none(),
803            name: Ident::new("Distance"),
804            type_: Type::Primitive(Primitive::Int),
805        }));
806        assert_eq!(rendered, "alias Distance = int;\n");
807    }
808
809    #[test]
810    fn an_anonymous_union_nests_inside_the_struct() {
811        let rendered = decl(&Decl::Struct(StructDecl {
812            doc: Doc::none(),
813            name: Ident::new("Payload"),
814            members: vec![
815                Member::Field(field("_d", Type::named("Mode"))),
816                Member::Union(vec![
817                    field("idle", Type::Primitive(Primitive::Int)),
818                    field("active", Type::Primitive(Primitive::Double)),
819                ]),
820            ],
821        }));
822        assert_eq!(
823            rendered,
824            "struct Payload\n{\n    Mode _d;\n    union\n    {\n        int idle;\n        \
825             double active;\n    }\n}\n"
826        );
827    }
828
829    #[test]
830    fn an_empty_union_member_writes_nothing() {
831        // A `union { }` with no fields says nothing and is a body a
832        // reader would have to check the grammar for.
833        let rendered = decl(&Decl::Struct(StructDecl {
834            doc: Doc::none(),
835            name: Ident::new("Empty"),
836            members: vec![Member::Union(Vec::new())],
837        }));
838        assert_eq!(rendered, "struct Empty\n{\n}\n");
839    }
840
841    #[test]
842    fn an_interface_writes_a_signature_per_method() {
843        let rendered = decl(&Decl::Interface(InterfaceDecl {
844            doc: Doc::none(),
845            name: Ident::new("Control"),
846            methods: vec![
847                Method {
848                    doc: Doc::none(),
849                    name: Ident::new("go"),
850                    returns: None,
851                    params: vec![Param {
852                        name: Ident::new("target"),
853                        type_: Type::named("Pose"),
854                    }],
855                },
856                Method {
857                    doc: Doc::none(),
858                    name: Ident::new("status"),
859                    returns: Some(Type::Primitive(Primitive::Int)),
860                    params: Vec::new(),
861                },
862            ],
863        }));
864        assert_eq!(
865            rendered,
866            "interface Control\n{\n    void go(Pose target);\n    int status();\n}\n"
867        );
868    }
869
870    #[test]
871    fn a_documentation_comment_uses_the_ddoc_marker() {
872        let rendered = decl(&Decl::Comment(Doc::lines(&["first", "", "third"])));
873        assert_eq!(rendered, "/// first\n///\n/// third\n");
874    }
875
876    #[test]
877    fn a_character_literal_is_written_as_its_code_point() {
878        assert_eq!(literal_text(&Literal::Char('a')), "97");
879    }
880
881    #[test]
882    fn a_string_literal_is_escaped_for_d() {
883        assert_eq!(
884            literal_text(&Literal::Str("a\"b\n".into())),
885            "\"a\\\"b\\n\""
886        );
887    }
888
889    #[test]
890    fn the_null_literal_is_ds_own_spelling() {
891        assert_eq!(literal_text(&Literal::Null), "null");
892    }
893
894    #[test]
895    fn a_shift_expression_renders_with_its_suffix() {
896        let expr = Expr::Binary {
897            op: "<<",
898            lhs: alloc::boxed::Box::new(Expr::suffixed_int(1, "UL")),
899            rhs: alloc::boxed::Box::new(Expr::int(63)),
900        };
901        assert_eq!(expr_text(&expr), "1UL << 63");
902    }
903
904    #[test]
905    fn a_cast_writes_its_target_in_parentheses() {
906        let expr = Expr::Cast {
907            type_: alloc::boxed::Box::new(Type::Primitive(Primitive::UByte)),
908            value: alloc::boxed::Box::new(Expr::int(3)),
909        };
910        assert_eq!(expr_text(&expr), "cast(ubyte)3");
911    }
912}