Skip to main content

zerodds_lang_render/
swift.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2026 ZeroDDS Contributors
3//! Rendering the Swift CST. Spec §5.
4//!
5//! One style, the one `swift-format` produces with its defaults: four
6//! spaces for indentation, the opening brace on the same line, `///` for
7//! documentation and `//` for a plain comment.
8//!
9//! Nothing is decided here. A Swift type reads name-first with the type
10//! after a colon and carries its own nesting, so unlike
11//! [`crate::c`] there is no declarator to split around a name:
12//! a property is `var`, the name, a colon and the type.
13//!
14//! Two spellings this renderer chooses and the reason for each:
15//!
16//! - [`Visibility::Internal`] writes **nothing**. `internal` is Swift's
17//!   default access level, so the keyword adds a word to every line and
18//!   changes no program.
19//! - [`Aggregate::Class`] writes `final class`. Nothing generated is
20//!   subclassed, and a non-final public class is an extension point the
21//!   IDL did not declare.
22
23use crate::writer::{Writer, comment_lines, escape_string, verbatim_lines};
24use alloc::string::{String, ToString};
25use alloc::vec::Vec;
26use zerodds_lang_cst::common::{Doc, Ident, Literal, Visibility};
27use zerodds_lang_cst::swift::{
28    Aggregate, AggregateDecl, Argument, ConstDecl, Decl, EnumCase, EnumDecl, Expr, File, FuncDecl,
29    Init, Method, Param, Property, ProtocolDecl, StaticLet, Stmt, Type, TypeAliasDecl,
30};
31
32/// Render a whole file.
33#[must_use]
34pub fn file(file: &File) -> String {
35    let mut w = Writer::spaces4();
36    if !file.doc.is_empty() {
37        comment_lines(&mut w, "// ", file.doc.as_lines());
38    }
39    if !file.imports.is_empty() {
40        w.blank_line();
41        for import in &file.imports {
42            let mut line = String::from("import ");
43            line.push_str(import.as_str());
44            w.line(&line);
45        }
46    }
47    for decl in &file.decls {
48        w.blank_line();
49        render_decl(&mut w, decl);
50    }
51    w.finish()
52}
53
54/// Render one declaration, for a caller that has only one.
55#[must_use]
56pub fn decl(node: &Decl) -> String {
57    let mut w = Writer::spaces4();
58    render_decl(&mut w, node);
59    w.finish()
60}
61
62/// Render one type, for a caller that has only one.
63#[must_use]
64pub fn type_(node: &Type) -> String {
65    type_text(node)
66}
67
68/// The identifier a name renders as, for a caller assembling one.
69#[must_use]
70pub fn ident(name: &Ident) -> String {
71    name.as_str().to_string()
72}
73
74fn render_decl(w: &mut Writer, node: &Decl) {
75    match node {
76        Decl::Aggregate(decl) => render_aggregate(w, decl),
77        Decl::Enum(decl) => render_enum(w, decl),
78        Decl::TypeAlias(decl) => render_type_alias(w, decl),
79        Decl::Constant(decl) => render_const(w, decl),
80        Decl::Protocol(decl) => render_protocol(w, decl),
81        Decl::Func(decl) => render_func(w, decl),
82        Decl::Comment(lines) => doc(w, lines),
83        Decl::Verbatim(text) => verbatim_lines(w, text),
84    }
85}
86
87fn render_func(w: &mut Writer, node: &FuncDecl) {
88    doc(w, &node.doc);
89    let mut head = visibility_text(node.visibility);
90    head.push_str("func ");
91    head.push_str(node.name.as_str());
92    head.push('(');
93    let params: Vec<String> = node.params.iter().map(param_text).collect();
94    head.push_str(&params.join(", "));
95    head.push(')');
96    if node.throws {
97        head.push_str(" throws");
98    }
99    if let Some(returns) = &node.returns {
100        head.push_str(" -> ");
101        head.push_str(&type_text(returns));
102    }
103    head.push_str(" {");
104    w.line(&head);
105    w.indent();
106    for stmt in &node.body {
107        render_stmt(w, stmt);
108    }
109    w.dedent();
110    w.line("}");
111}
112
113fn render_aggregate(w: &mut Writer, node: &AggregateDecl) {
114    doc(w, &node.doc);
115    let mut head = visibility_text(node.visibility);
116    head.push_str(match node.kind {
117        Aggregate::Struct => "struct ",
118        Aggregate::Class => "final class ",
119    });
120    head.push_str(node.name.as_str());
121    if !node.generics.is_empty() {
122        head.push('<');
123        let parts: Vec<&str> = node.generics.iter().map(Ident::as_str).collect();
124        head.push_str(&parts.join(", "));
125        head.push('>');
126    }
127    if !node.conformances.is_empty() {
128        head.push_str(": ");
129        let parts: Vec<&str> = node.conformances.iter().map(Ident::as_str).collect();
130        head.push_str(&parts.join(", "));
131    }
132    head.push_str(" {");
133    w.line(&head);
134    w.indent();
135    for text in &node.body_text.begin {
136        verbatim_lines(w, text);
137    }
138    for property in &node.properties {
139        render_property(w, property);
140    }
141    for static_ in &node.statics {
142        render_static(w, static_);
143    }
144    for init in &node.inits {
145        render_init(w, init);
146    }
147    for text in &node.body_text.end {
148        verbatim_lines(w, text);
149    }
150    w.dedent();
151    w.line("}");
152}
153
154fn render_property(w: &mut Writer, node: &Property) {
155    doc(w, &node.doc);
156    let mut line = visibility_text(node.visibility);
157    line.push_str(if node.mutable { "var " } else { "let " });
158    line.push_str(node.name.as_str());
159    line.push_str(": ");
160    line.push_str(&type_text(&node.type_));
161    w.line(&line);
162}
163
164fn render_static(w: &mut Writer, node: &StaticLet) {
165    doc(w, &node.doc);
166    let mut line = visibility_text(node.visibility);
167    line.push_str("static let ");
168    line.push_str(node.name.as_str());
169    if let Some(type_) = &node.type_ {
170        line.push_str(": ");
171        line.push_str(&type_text(type_));
172    }
173    line.push_str(" = ");
174    line.push_str(&expr_text(&node.value));
175    w.line(&line);
176}
177
178/// An initialiser.
179///
180/// An empty body renders as `init() {}` on one line, which is what
181/// `swift-format` produces for a body with no statements.
182fn render_init(w: &mut Writer, node: &Init) {
183    doc(w, &node.doc);
184    let mut head = visibility_text(node.visibility);
185    head.push_str("init(");
186    let params: Vec<String> = node.params.iter().map(param_text).collect();
187    head.push_str(&params.join(", "));
188    head.push(')');
189    if node.body.is_empty() {
190        head.push_str(" {}");
191        w.line(&head);
192        return;
193    }
194    head.push_str(" {");
195    w.line(&head);
196    w.indent();
197    for stmt in &node.body {
198        render_stmt(w, stmt);
199    }
200    w.dedent();
201    w.line("}");
202}
203
204/// zerodds-lint: recursion-depth 64 (one level per nested block of a
205/// generated body. The codec walk emits a routine per type rather than
206/// inlining one — `lang-rules/src/emit/encode.rs` — so a body nests by
207/// its own framing and by one loop per array dimension, never by the
208/// type graph, whose descent stops at 64 anyway; the else-if chain, one
209/// arm per union case, is walked iteratively and costs no depth.)
210fn render_stmt(w: &mut Writer, node: &Stmt) {
211    match node {
212        Stmt::AssignProperty { property, value } => {
213            let mut line = String::from("self.");
214            line.push_str(property.as_str());
215            line.push_str(" = ");
216            line.push_str(&expr_text(value));
217            w.line(&line);
218        }
219        Stmt::Var { name, type_, value } => {
220            let mut line = String::from("var ");
221            line.push_str(name.as_str());
222            if let Some(type_) = type_ {
223                line.push_str(": ");
224                line.push_str(&type_text(type_));
225            }
226            if let Some(value) = value {
227                line.push_str(" = ");
228                line.push_str(&expr_text(value));
229            }
230            w.line(&line);
231        }
232        Stmt::Assign { place, value } => {
233            let mut line = expr_text(place);
234            line.push_str(" = ");
235            line.push_str(&expr_text(value));
236            w.line(&line);
237        }
238        Stmt::Expr(value) => w.line(&expr_text(value)),
239        Stmt::Return(value) => match value {
240            Some(value) => {
241                let mut line = String::from("return ");
242                line.push_str(&expr_text(value));
243                w.line(&line);
244            }
245            None => w.line("return"),
246        },
247        Stmt::If {
248            condition,
249            then,
250            otherwise,
251        } => render_if(w, condition, then, otherwise.as_deref()),
252        Stmt::ForRange {
253            counter,
254            bound,
255            body,
256        } => {
257            let mut head = String::from("for ");
258            head.push_str(counter.as_str());
259            head.push_str(" in 0..<");
260            head.push_str(&tight_operand(bound));
261            head.push_str(" {");
262            w.line(&head);
263            w.indent();
264            for stmt in body {
265                render_stmt(w, stmt);
266            }
267            w.dedent();
268            w.line("}");
269        }
270        Stmt::While { condition, body } => {
271            let mut head = String::from("while ");
272            head.push_str(&expr_text(condition));
273            head.push_str(" {");
274            w.line(&head);
275            w.indent();
276            for stmt in body {
277                render_stmt(w, stmt);
278            }
279            w.dedent();
280            w.line("}");
281        }
282        Stmt::Break => w.line("break"),
283        Stmt::Comment(lines) => {
284            if !lines.is_empty() {
285                comment_lines(w, "// ", lines.as_lines());
286            }
287        }
288    }
289}
290
291/// An `if`, with an `else` whose whole body is one more `if` written
292/// as `} else if ... {` — the shape a union's arm chain reads best in,
293/// and what keeps a long chain from indenting off the page.
294fn render_if(w: &mut Writer, condition: &Expr, then: &[Stmt], otherwise: Option<&[Stmt]>) {
295    let mut head = String::from("if ");
296    head.push_str(&expr_text(condition));
297    head.push_str(" {");
298    w.line(&head);
299    render_if_tail(w, then, otherwise);
300}
301
302/// The next arm is taken in a loop and not by a call to this function
303/// again. The depth this used to claim — 256 — was nothing's: the
304/// chain's length is the union's case count, the enumeration's
305/// enumerator count or the `@mutable` aggregate's member count, the walk
306/// in `lang-rules/src/emit/encode.rs` builds one `If` per arm in the
307/// previous one's `else`, and nothing in the model bounds any of the
308/// three.
309///
310/// zerodds-lint: recursion-depth 64 (block nesting only; the arm chain
311/// is walked iteratively, and what remains is one level per nested block
312/// of a generated body — the codec walk emits a routine per type rather
313/// than inlining, so a body nests by its framing and by one loop per
314/// array dimension, never by the type graph)
315fn render_if_tail(w: &mut Writer, then: &[Stmt], otherwise: Option<&[Stmt]>) {
316    let mut then = then;
317    let mut otherwise = otherwise;
318    loop {
319        w.indent();
320        for stmt in then {
321            render_stmt(w, stmt);
322        }
323        w.dedent();
324        let Some(branch) = otherwise else {
325            w.line("}");
326            return;
327        };
328        if let [
329            Stmt::If {
330                condition: next_condition,
331                then: next_then,
332                otherwise: next_otherwise,
333            },
334        ] = branch
335        {
336            let mut head = String::from("} else if ");
337            head.push_str(&expr_text(next_condition));
338            head.push_str(" {");
339            w.line(&head);
340            then = next_then;
341            otherwise = next_otherwise.as_deref();
342            continue;
343        }
344        w.line("} else {");
345        w.indent();
346        for stmt in branch {
347            render_stmt(w, stmt);
348        }
349        w.dedent();
350        w.line("}");
351        return;
352    }
353}
354
355/// A bound as a `0..<` operand: a binary expression, or a `try`
356/// carried by one, is bracketed so the range operator does not bind
357/// into it.
358fn tight_operand(bound: &Expr) -> String {
359    match bound {
360        Expr::Binary { .. } | Expr::Try(_) => {
361            let mut out = String::from("(");
362            out.push_str(&expr_text(bound));
363            out.push(')');
364            out
365        }
366        other => expr_text(other),
367    }
368}
369
370fn param_text(param: &Param) -> String {
371    let mut out = String::from(param.label.as_str());
372    if let Some(internal) = &param.internal_name {
373        out.push(' ');
374        out.push_str(internal.as_str());
375    }
376    out.push_str(": ");
377    out.push_str(&type_text(&param.type_));
378    out
379}
380
381fn render_enum(w: &mut Writer, node: &EnumDecl) {
382    doc(w, &node.doc);
383    let mut head = visibility_text(node.visibility);
384    head.push_str("enum ");
385    head.push_str(node.name.as_str());
386    if let Some(raw) = &node.raw_type {
387        head.push_str(": ");
388        head.push_str(&type_text(raw));
389    }
390    head.push_str(" {");
391    w.line(&head);
392    w.indent();
393    for case in &node.cases {
394        render_case(w, case);
395    }
396    w.dedent();
397    w.line("}");
398}
399
400fn render_case(w: &mut Writer, node: &EnumCase) {
401    doc(w, &node.doc);
402    let mut line = String::from("case ");
403    line.push_str(node.name.as_str());
404    if let Some(value) = &node.raw_value {
405        line.push_str(" = ");
406        line.push_str(&expr_text(value));
407    }
408    w.line(&line);
409}
410
411fn render_type_alias(w: &mut Writer, node: &TypeAliasDecl) {
412    doc(w, &node.doc);
413    let mut line = visibility_text(node.visibility);
414    line.push_str("typealias ");
415    line.push_str(node.name.as_str());
416    line.push_str(" = ");
417    line.push_str(&type_text(&node.type_));
418    w.line(&line);
419}
420
421fn render_const(w: &mut Writer, node: &ConstDecl) {
422    doc(w, &node.doc);
423    let mut line = visibility_text(node.visibility);
424    line.push_str("let ");
425    line.push_str(node.name.as_str());
426    if let Some(type_) = &node.type_ {
427        line.push_str(": ");
428        line.push_str(&type_text(type_));
429    }
430    line.push_str(" = ");
431    line.push_str(&expr_text(&node.value));
432    w.line(&line);
433}
434
435fn render_protocol(w: &mut Writer, node: &ProtocolDecl) {
436    doc(w, &node.doc);
437    let mut head = visibility_text(node.visibility);
438    head.push_str("protocol ");
439    head.push_str(node.name.as_str());
440    head.push_str(" {");
441    w.line(&head);
442    w.indent();
443    for method in &node.methods {
444        render_method(w, method);
445    }
446    w.dedent();
447    w.line("}");
448}
449
450fn render_method(w: &mut Writer, node: &Method) {
451    doc(w, &node.doc);
452    let mut line = String::from("func ");
453    line.push_str(node.name.as_str());
454    line.push('(');
455    let params: Vec<String> = node.params.iter().map(param_text).collect();
456    line.push_str(&params.join(", "));
457    line.push(')');
458    if node.throws {
459        line.push_str(" throws");
460    }
461    if let Some(returns) = &node.returns {
462        line.push_str(" -> ");
463        line.push_str(&type_text(returns));
464    }
465    w.line(&line);
466}
467
468/// zerodds-lint: recursion-depth 64 (one level per collection, alias or
469/// generic argument of the spelled type; `MAX_TYPE_DEPTH` in
470/// `lang-rules/src/emit/swift.rs` refuses a type that nests deeper)
471fn type_text(node: &Type) -> String {
472    match node {
473        Type::Primitive(primitive) => primitive.as_str().to_string(),
474        Type::Named(name) => name.as_str().to_string(),
475        Type::Generic { base, arguments } => {
476            let mut out = String::from(base.as_str());
477            out.push('<');
478            let parts: Vec<String> = arguments.iter().map(type_text).collect();
479            out.push_str(&parts.join(", "));
480            out.push('>');
481            out
482        }
483        Type::Array(element) => {
484            let mut out = String::from("[");
485            out.push_str(&type_text(element));
486            out.push(']');
487            out
488        }
489        Type::Optional(inner) => {
490            let mut out = type_text(inner);
491            out.push('?');
492            out
493        }
494    }
495}
496
497/// zerodds-lint: recursion-depth = the built expression's nesting. A walk
498/// calls a routine per type rather than nesting one, so the one operand
499/// tree that grows with the IDL is an array's default value: one composite
500/// literal per dimension, built in `lang-rules/src/default_swift.rs`, and
501/// nothing bounds the dimension count. No number is claimed: this
502/// signature returns no error.
503fn expr_text(node: &Expr) -> String {
504    match node {
505        Expr::Literal(literal) => literal_text(literal),
506        Expr::Name(name) => name.as_str().to_string(),
507        Expr::Binary { op, lhs, rhs } => {
508            let mut out = expr_text(lhs);
509            out.push(' ');
510            out.push_str(op);
511            out.push(' ');
512            out.push_str(&expr_text(rhs));
513            out
514        }
515        Expr::Call { callee, args } => {
516            let mut out = String::from(callee.as_str());
517            out.push('(');
518            let parts: Vec<String> = args.iter().map(argument_text).collect();
519            out.push_str(&parts.join(", "));
520            out.push(')');
521            out
522        }
523        Expr::Paren(inner) => {
524            let mut out = String::from("(");
525            out.push_str(&expr_text(inner));
526            out.push(')');
527            out
528        }
529        Expr::Member { value, name } => {
530            let mut out = expr_text(value);
531            out.push('.');
532            out.push_str(name.as_str());
533            out
534        }
535        Expr::Invoke {
536            receiver,
537            name,
538            args,
539        } => {
540            let mut out = match receiver {
541                Some(receiver) => {
542                    let mut head = expr_text(receiver);
543                    head.push('.');
544                    head
545                }
546                None => String::new(),
547            };
548            out.push_str(name.as_str());
549            out.push('(');
550            let parts: Vec<String> = args.iter().map(argument_text).collect();
551            out.push_str(&parts.join(", "));
552            out.push(')');
553            out
554        }
555        Expr::Index { value, index } => {
556            let mut out = expr_text(value);
557            out.push('[');
558            out.push_str(&expr_text(index));
559            out.push(']');
560            out
561        }
562        Expr::Unary { op, value } => {
563            let mut out = String::from(*op);
564            out.push_str(&expr_text(value));
565            out
566        }
567        Expr::ForceUnwrap(value) => {
568            let mut out = expr_text(value);
569            out.push('!');
570            out
571        }
572        Expr::ArrayLit(elements) => {
573            let mut out = String::from("[");
574            let parts: Vec<String> = elements.iter().map(expr_text).collect();
575            out.push_str(&parts.join(", "));
576            out.push(']');
577            out
578        }
579        Expr::RepeatingArray {
580            element,
581            repeated,
582            count,
583        } => {
584            let mut out = String::from("[");
585            out.push_str(&type_text(element));
586            out.push_str("](repeating: ");
587            out.push_str(&expr_text(repeated));
588            out.push_str(", count: ");
589            out.push_str(&expr_text(count));
590            out.push(')');
591            out
592        }
593        Expr::TypeSelf(type_) => {
594            let mut out = type_text(type_);
595            out.push_str(".self");
596            out
597        }
598        // `try` at the very front of what it wraps — the CST only ever
599        // builds one of these around the outermost expression a
600        // throwing call reached, never around a bare operand, so no
601        // parenthesising is needed here for `try` to bind correctly.
602        Expr::Try(inner) => {
603            let mut out = String::from("try ");
604            out.push_str(&expr_text(inner));
605            out
606        }
607    }
608}
609
610fn argument_text(arg: &Argument) -> String {
611    let mut out = String::new();
612    if let Some(label) = &arg.label {
613        out.push_str(label.as_str());
614        out.push_str(": ");
615    }
616    out.push_str(&expr_text(&arg.value));
617    out
618}
619
620fn literal_text(literal: &Literal) -> String {
621    match literal {
622        Literal::Int(text) | Literal::Float(text) => text.clone(),
623        Literal::Bool(value) => {
624            if *value {
625                "true".to_string()
626            } else {
627                "false".to_string()
628            }
629        }
630        // The code point as a number. A Swift `Character` is an extended
631        // grapheme cluster and an IDL `char` is one octet, so a quoted
632        // literal would not be the same value; the member's own type —
633        // `UInt8` for `char`, `UInt16` for `wchar` — is what the number
634        // is written into.
635        Literal::Char(ch) => alloc::format!("{}", u32::from(*ch)),
636        Literal::Str(text) => {
637            let mut out = String::from("\"");
638            out.push_str(&escape_string(text));
639            out.push('"');
640            out
641        }
642        Literal::Bytes(bytes) => {
643            let mut out = String::from("[");
644            let parts: Vec<String> = bytes.iter().map(|b| alloc::format!("0x{b:02X}")).collect();
645            out.push_str(&parts.join(", "));
646            out.push(']');
647            out
648        }
649        Literal::Null => "nil".to_string(),
650    }
651}
652
653/// The access-level keyword, with a trailing space where there is one.
654///
655/// `internal` writes nothing: it is Swift's default access level, so the
656/// keyword would be a word on every line that changes no program.
657fn visibility_text(visibility: Visibility) -> String {
658    match visibility {
659        Visibility::Public => "public ".to_string(),
660        Visibility::Internal => String::new(),
661        Visibility::Private => "private ".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 zerodds_lang_cst::swift::Primitive;
677
678    fn int32() -> Type {
679        Type::Primitive(Primitive::Int32)
680    }
681
682    #[test]
683    fn a_struct_writes_its_properties_and_its_public_initialiser() {
684        let decl = Decl::Aggregate(AggregateDecl {
685            doc: Doc::none(),
686            visibility: Visibility::Public,
687            kind: Aggregate::Struct,
688            name: Ident::new("Pose"),
689            generics: Vec::new(),
690            conformances: Vec::new(),
691            properties: alloc::vec![Property {
692                doc: Doc::none(),
693                visibility: Visibility::Public,
694                mutable: true,
695                name: Ident::new("x"),
696                type_: int32(),
697            }],
698            statics: Vec::new(),
699            body_text: zerodds_lang_cst::common::BodyText::none(),
700            inits: alloc::vec![Init {
701                doc: Doc::none(),
702                visibility: Visibility::Public,
703                params: alloc::vec![Param {
704                    label: Ident::new("x"),
705                    internal_name: None,
706                    type_: int32(),
707                }],
708                body: alloc::vec![Stmt::AssignProperty {
709                    property: Ident::new("x"),
710                    value: Expr::Name(Ident::new("x")),
711                }],
712            }],
713        });
714        assert_eq!(
715            super::decl(&decl),
716            "public struct Pose {\n    public var x: Int32\n    public init(x: Int32) {\n        \
717             self.x = x\n    }\n}\n"
718        );
719    }
720
721    #[test]
722    fn an_empty_initialiser_stays_on_one_line() {
723        let init = Init {
724            doc: Doc::none(),
725            visibility: Visibility::Public,
726            params: Vec::new(),
727            body: Vec::new(),
728        };
729        let mut w = Writer::spaces4();
730        render_init(&mut w, &init);
731        assert_eq!(w.finish(), "public init() {}\n");
732    }
733
734    #[test]
735    fn a_parameter_with_a_second_name_writes_both() {
736        let param = Param {
737            label: Ident::new("`self`"),
738            internal_name: Some(Ident::new("zdSelf")),
739            type_: int32(),
740        };
741        assert_eq!(param_text(&param), "`self` zdSelf: Int32");
742    }
743
744    #[test]
745    fn a_class_is_written_final() {
746        let decl = Decl::Aggregate(AggregateDecl {
747            doc: Doc::none(),
748            visibility: Visibility::Public,
749            kind: Aggregate::Class,
750            name: Ident::new("ZdExternal"),
751            generics: alloc::vec![Ident::new("Value")],
752            conformances: Vec::new(),
753            properties: Vec::new(),
754            statics: Vec::new(),
755            body_text: zerodds_lang_cst::common::BodyText::none(),
756            inits: Vec::new(),
757        });
758        assert_eq!(
759            super::decl(&decl),
760            "public final class ZdExternal<Value> {\n}\n"
761        );
762    }
763
764    #[test]
765    fn an_option_set_writes_its_conformance_and_its_flags() {
766        let decl = Decl::Aggregate(AggregateDecl {
767            doc: Doc::none(),
768            visibility: Visibility::Public,
769            kind: Aggregate::Struct,
770            name: Ident::new("Flags"),
771            generics: Vec::new(),
772            conformances: alloc::vec![Ident::new("OptionSet")],
773            properties: Vec::new(),
774            statics: alloc::vec![StaticLet {
775                doc: Doc::none(),
776                visibility: Visibility::Public,
777                name: Ident::new("READY"),
778                type_: None,
779                value: Expr::Call {
780                    callee: Ident::new("Flags"),
781                    args: alloc::vec![Argument {
782                        label: Some(Ident::new("rawValue")),
783                        value: Expr::Binary {
784                            op: "<<",
785                            lhs: alloc::boxed::Box::new(Expr::int(1)),
786                            rhs: alloc::boxed::Box::new(Expr::int(4)),
787                        },
788                    }],
789                },
790            }],
791            body_text: zerodds_lang_cst::common::BodyText::none(),
792            inits: Vec::new(),
793        });
794        assert_eq!(
795            super::decl(&decl),
796            "public struct Flags: OptionSet {\n    public static let READY = \
797             Flags(rawValue: 1 << 4)\n}\n"
798        );
799    }
800
801    #[test]
802    fn an_enum_writes_every_raw_value_out() {
803        let decl = Decl::Enum(EnumDecl {
804            doc: Doc::none(),
805            visibility: Visibility::Public,
806            name: Ident::new("Mode"),
807            raw_type: Some(int32()),
808            cases: alloc::vec![
809                EnumCase {
810                    doc: Doc::none(),
811                    name: Ident::new("IDLE"),
812                    raw_value: Some(Expr::int(0)),
813                },
814                EnumCase {
815                    doc: Doc::none(),
816                    name: Ident::new("ACTIVE"),
817                    raw_value: Some(Expr::int(4)),
818                },
819            ],
820        });
821        assert_eq!(
822            super::decl(&decl),
823            "public enum Mode: Int32 {\n    case IDLE = 0\n    case ACTIVE = 4\n}\n"
824        );
825    }
826
827    #[test]
828    fn an_enum_with_no_cases_writes_no_raw_type() {
829        let decl = Decl::Enum(EnumDecl {
830            doc: Doc::none(),
831            visibility: Visibility::Public,
832            name: Ident::new("Empty"),
833            raw_type: None,
834            cases: Vec::new(),
835        });
836        assert_eq!(super::decl(&decl), "public enum Empty {\n}\n");
837    }
838
839    #[test]
840    fn a_protocol_writes_its_requirements() {
841        let decl = Decl::Protocol(ProtocolDecl {
842            doc: Doc::none(),
843            visibility: Visibility::Public,
844            name: Ident::new("Control"),
845            methods: alloc::vec![
846                Method {
847                    doc: Doc::none(),
848                    name: Ident::new("go"),
849                    params: alloc::vec![Param {
850                        label: Ident::new("target"),
851                        internal_name: None,
852                        type_: Type::named("Pose"),
853                    }],
854                    throws: false,
855                    returns: None,
856                },
857                Method {
858                    doc: Doc::none(),
859                    name: Ident::new("status"),
860                    params: Vec::new(),
861                    throws: false,
862                    returns: Some(int32()),
863                },
864            ],
865        });
866        assert_eq!(
867            super::decl(&decl),
868            "public protocol Control {\n    func go(target: Pose)\n    func status() -> Int32\n}\n"
869        );
870    }
871
872    #[test]
873    fn a_typealias_and_a_constant_each_write_one_line() {
874        assert_eq!(
875            super::decl(&Decl::TypeAlias(TypeAliasDecl {
876                doc: Doc::none(),
877                visibility: Visibility::Public,
878                name: Ident::new("Distance"),
879                type_: int32(),
880            })),
881            "public typealias Distance = Int32\n"
882        );
883        assert_eq!(
884            super::decl(&Decl::Constant(ConstDecl {
885                doc: Doc::none(),
886                visibility: Visibility::Public,
887                name: Ident::new("LIMIT"),
888                type_: Some(Type::Primitive(Primitive::UInt8)),
889                value: Expr::int(8),
890            })),
891            "public let LIMIT: UInt8 = 8\n"
892        );
893    }
894
895    #[test]
896    fn a_collection_type_writes_the_sugar_and_not_the_generic() {
897        // `[T]` means `Swift.Array<T>` whatever else the file declares,
898        // and `T?` means `Swift.Optional<T>`. A generated type called
899        // `Array` cannot capture either.
900        assert_eq!(type_(&Type::array(int32())), "[Int32]");
901        assert_eq!(type_(&Type::array(Type::array(int32()))), "[[Int32]]");
902        assert_eq!(type_(&Type::optional(int32())), "Int32?");
903        assert_eq!(type_(&Type::optional(Type::array(int32()))), "[Int32]?");
904    }
905
906    #[test]
907    fn a_generic_type_writes_its_arguments() {
908        assert_eq!(
909            type_(&Type::generic(
910                "ZdExternal",
911                alloc::vec![Type::named("Node")]
912            )),
913            "ZdExternal<Node>"
914        );
915    }
916
917    #[test]
918    fn an_internal_declaration_writes_no_keyword_because_that_is_the_default() {
919        assert_eq!(visibility_text(Visibility::Internal), "");
920        assert_eq!(visibility_text(Visibility::Public), "public ");
921        assert_eq!(visibility_text(Visibility::Private), "private ");
922    }
923
924    #[test]
925    fn a_string_literal_is_escaped_and_a_char_is_its_code_point() {
926        assert_eq!(
927            literal_text(&Literal::Str("a\"b".to_string())),
928            "\"a\\\"b\""
929        );
930        assert_eq!(literal_text(&Literal::Char('A')), "65");
931        assert_eq!(literal_text(&Literal::Null), "nil");
932        assert_eq!(literal_text(&Literal::Bool(true)), "true");
933    }
934
935    #[test]
936    fn a_file_writes_its_header_as_a_plain_comment() {
937        let out = file(&File {
938            doc: Doc::lines(&["Generated by ZeroDDS. Do not edit."]),
939            imports: alloc::vec![Ident::new("Foundation")],
940            decls: Vec::new(),
941        });
942        assert_eq!(
943            out,
944            "// Generated by ZeroDDS. Do not edit.\n\nimport Foundation\n"
945        );
946    }
947}