Skip to main content

zerodds_lang_render/
ada.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2026 ZeroDDS Contributors
3//! Rendering the Ada CST. Spec §5.
4//!
5//! One style: three spaces per level, `--` comments, one declaration per
6//! line, the reserved words in lower case and the identifiers in
7//! `Mixed_Case_With_Underscores`. Three spaces because that is what the
8//! Ada Reference Manual's own examples use and what GNAT's `-gnaty`
9//! style checks expect; nothing in the language depends on it.
10//!
11//! Nothing is decided here. A record either has components or is written
12//! `null record`, and which of the two it is was decided by
13//! [`zerodds_lang_cst::ada::Record::is_null`] — the renderer only asks.
14//!
15//! Two differences from every other renderer in this crate, and both are
16//! Ada's:
17//!
18//! 1. **A string literal doubles its quote.** Ada 2012 RM 2.6 spells an
19//!    embedded quotation mark `""`, where the C family writes `\"`, so
20//!    [`crate::writer::escape_string`] is not used here at all — it
21//!    would produce a backslash Ada reads as a graphic character of its
22//!    own.
23//! 2. **A comment marker is `--` and has no block form.** RM 2.7: a
24//!    comment runs to the end of the line and there is no closing
25//!    delimiter, so a comment can never swallow the declaration after
26//!    it.
27
28use crate::writer::{Writer, comment_lines, verbatim_lines};
29use alloc::string::{String, ToString};
30use alloc::vec::Vec;
31use zerodds_lang_cst::ada::{
32    AllocInit, Arm, Choice, Component, Decl, Discriminant, EnumRep, Expr, File, Index, Mode,
33    ObjectDecl, Param, Stmt, Subprogram, SubprogramBody, SubprogramKind, SubtypeDecl, TypeDecl,
34    TypeDef, VarDecl, Variant,
35};
36use zerodds_lang_cst::common::{Doc, Ident, Literal, Path};
37
38/// The indentation one level costs.
39const UNIT: &str = "   ";
40
41/// Render a whole package specification.
42#[must_use]
43pub fn file(file: &File) -> String {
44    let mut w = Writer::new(UNIT);
45    doc(&mut w, &file.doc);
46    if !file.withs.is_empty() || !file.use_types.is_empty() {
47        if !file.doc.is_empty() {
48            w.blank_line();
49        }
50        for unit in &file.withs {
51            let mut line = String::from("with ");
52            line.push_str(&path_text(unit));
53            line.push(';');
54            w.line(&line);
55        }
56        // After the `with` clauses, because a `use_type_clause` names a
57        // subtype the unit has to be able to see (RM 8.4), and it is the
58        // `with` that makes it visible. Both are context items
59        // (RM 10.1.2), so both stand above the package.
60        for mark in &file.use_types {
61            let mut line = String::from("use type ");
62            line.push_str(&path_text(mark));
63            line.push(';');
64            w.line(&line);
65        }
66    }
67    w.blank_line();
68    let mut head = String::from("package ");
69    head.push_str(file.package.as_str());
70    head.push_str(" is");
71    w.line(&head);
72    w.indent();
73    for decl in &file.decls {
74        w.blank_line();
75        render_decl(&mut w, decl);
76    }
77    w.dedent();
78    w.blank_line();
79    let mut tail = String::from("end ");
80    tail.push_str(file.package.as_str());
81    tail.push(';');
82    w.line(&tail);
83    w.finish()
84}
85
86/// Render a package body: `package body Name is … end Name;`.
87///
88/// The second half of what a generated Ada unit is. A subprogram body
89/// is not a declaration a package *specification* may carry (RM 7.1
90/// admits `basic_declarative_item`s, and a `body` is not one), so the
91/// codec's bodies stand here and the profiles that name them stand in
92/// the spec. The `with` clauses are the spec's: RM 10.1.2 extends a
93/// context clause of a library unit declaration over the corresponding
94/// body, so a body that repeated them would say the same thing twice.
95#[must_use]
96pub fn package_body(package: &Ident, decls: &[Decl]) -> String {
97    let mut w = Writer::new(UNIT);
98    let mut head = String::from("package body ");
99    head.push_str(package.as_str());
100    head.push_str(" is");
101    w.line(&head);
102    w.indent();
103    for decl in decls {
104        w.blank_line();
105        render_decl(&mut w, decl);
106    }
107    w.dedent();
108    w.blank_line();
109    let mut tail = String::from("end ");
110    tail.push_str(package.as_str());
111    tail.push(';');
112    w.line(&tail);
113    w.finish()
114}
115
116/// Render one declaration, for a caller that has only one.
117#[must_use]
118pub fn decl(node: &Decl) -> String {
119    let mut w = Writer::new(UNIT);
120    render_decl(&mut w, node);
121    w.finish()
122}
123
124/// Render one expression, for a caller assembling one.
125#[must_use]
126pub fn expr(node: &Expr) -> String {
127    expr_text(node)
128}
129
130/// The identifier a name renders as, for a caller assembling one.
131#[must_use]
132pub fn ident(name: &Ident) -> String {
133    name.as_str().to_string()
134}
135
136/// A qualified name, joined with the selector Ada uses.
137#[must_use]
138pub fn path(name: &Path) -> String {
139    path_text(name)
140}
141
142fn path_text(name: &Path) -> String {
143    name.segments()
144        .iter()
145        .map(|segment| segment.as_str())
146        .collect::<Vec<&str>>()
147        .join(".")
148}
149
150/// Render one statement, for a test that holds only one.
151#[must_use]
152pub fn stmt(node: &Stmt) -> String {
153    let mut w = Writer::new(UNIT);
154    render_stmts(&mut w, core::slice::from_ref(node));
155    w.finish()
156}
157
158/// Render a statement list, `declare` blocks folded in.
159#[must_use]
160pub fn stmts(nodes: &[Stmt]) -> String {
161    let mut w = Writer::new(UNIT);
162    render_stmts(&mut w, nodes);
163    w.finish()
164}
165
166fn render_decl(w: &mut Writer, node: &Decl) {
167    match node {
168        Decl::Type(node) => render_type_decl(w, node),
169        Decl::Subtype(node) => render_subtype(w, node),
170        Decl::Object(node) => render_object(w, node),
171        Decl::EnumRep(node) => render_enum_rep(w, node),
172        Decl::Subprogram(node) => render_subprogram(w, node),
173        Decl::SubprogramBody(node) => render_subprogram_body(w, node),
174        Decl::Comment(lines) => doc(w, lines),
175        Decl::Verbatim(text) => verbatim_lines(w, text),
176    }
177}
178
179fn render_type_decl(w: &mut Writer, node: &TypeDecl) {
180    doc(w, &node.doc);
181    let mut head = String::from("type ");
182    head.push_str(node.name.as_str());
183    match &node.def {
184        TypeDef::Record(record) => {
185            head.push_str(&discriminant_text(&record.discriminants));
186            if record.is_null() {
187                head.push_str(" is null record;");
188                w.line(&head);
189                return;
190            }
191            head.push_str(" is record");
192            w.line(&head);
193            w.indent();
194            for text in &record.body_text.begin {
195                verbatim_lines(w, text);
196            }
197            for component in &record.components {
198                render_component(w, component);
199            }
200            if let Some(variant) = &record.variant {
201                render_variant(w, variant);
202            }
203            for text in &record.body_text.end {
204                verbatim_lines(w, text);
205            }
206            w.dedent();
207            w.line("end record;");
208        }
209        TypeDef::Enumeration(literals) => {
210            head.push_str(" is (");
211            head.push_str(
212                &literals
213                    .iter()
214                    .map(|literal| literal.as_str())
215                    .collect::<Vec<&str>>()
216                    .join(", "),
217            );
218            head.push_str(");");
219            w.line(&head);
220        }
221        TypeDef::Derived(mark) => {
222            head.push_str(" is new ");
223            head.push_str(&path_text(mark));
224            head.push(';');
225            w.line(&head);
226        }
227        TypeDef::Array { index, element } => {
228            head.push_str(" is array (");
229            head.push_str(
230                &index
231                    .iter()
232                    .map(index_text)
233                    .collect::<Vec<String>>()
234                    .join(", "),
235            );
236            head.push_str(") of ");
237            head.push_str(&path_text(element));
238            head.push(';');
239            w.line(&head);
240        }
241        TypeDef::Access(designated) => {
242            head.push_str(" is access ");
243            head.push_str(&path_text(designated));
244            head.push(';');
245            w.line(&head);
246        }
247        TypeDef::Interface => {
248            head.push_str(" is interface;");
249            w.line(&head);
250        }
251        // RM 3.10.1: the name and nothing else. The completion is a
252        // later declaration in the same declarative part.
253        TypeDef::Incomplete => {
254            head.push(';');
255            w.line(&head);
256        }
257    }
258}
259
260/// `(D : Mode := IDLE)`, or nothing.
261fn discriminant_text(discriminants: &[Discriminant]) -> String {
262    if discriminants.is_empty() {
263        return String::new();
264    }
265    let parts: Vec<String> = discriminants
266        .iter()
267        .map(|discriminant| {
268            let mut out = String::from(discriminant.name.as_str());
269            out.push_str(" : ");
270            out.push_str(&path_text(&discriminant.type_));
271            if let Some(default) = &discriminant.default {
272                out.push_str(" := ");
273                out.push_str(&expr_text(default));
274            }
275            out
276        })
277        .collect();
278    let mut out = String::from(" (");
279    out.push_str(&parts.join("; "));
280    out.push(')');
281    out
282}
283
284fn render_component(w: &mut Writer, node: &Component) {
285    doc(w, &node.doc);
286    let mut line = String::from(node.name.as_str());
287    line.push_str(" : ");
288    line.push_str(&path_text(&node.type_));
289    line.push(';');
290    w.line(&line);
291}
292
293/// `case D is … end case;`
294///
295/// An arm with no components is written `null;` — RM 3.8 gives `null` as
296/// a `component_list` of its own, and an arm with nothing at all between
297/// `=>` and the next `when` is not Ada.
298fn render_variant(w: &mut Writer, node: &Variant) {
299    let mut head = String::from("case ");
300    head.push_str(node.discriminant.as_str());
301    head.push_str(" is");
302    w.line(&head);
303    w.indent();
304    for arm in &node.arms {
305        render_arm(w, arm);
306    }
307    w.dedent();
308    w.line("end case;");
309}
310
311fn render_arm(w: &mut Writer, node: &Arm) {
312    let mut head = String::from("when ");
313    head.push_str(
314        &node
315            .choices
316            .iter()
317            .map(choice_text)
318            .collect::<Vec<String>>()
319            .join(" | "),
320    );
321    head.push_str(" =>");
322    w.line(&head);
323    w.indent();
324    if node.components.is_empty() {
325        w.line("null;");
326    } else {
327        for component in &node.components {
328            render_component(w, component);
329        }
330    }
331    w.dedent();
332}
333
334fn choice_text(node: &Choice) -> String {
335    match node {
336        Choice::Value(value) => expr_text(value),
337        Choice::Others => "others".to_string(),
338    }
339}
340
341fn index_text(node: &Index) -> String {
342    match node {
343        Index::Range { low, high } => {
344            let mut out = expr_text(low);
345            out.push_str(" .. ");
346            out.push_str(&expr_text(high));
347            out
348        }
349        Index::Unconstrained(mark) => {
350            let mut out = path_text(mark);
351            out.push_str(" range <>");
352            out
353        }
354    }
355}
356
357fn render_subtype(w: &mut Writer, node: &SubtypeDecl) {
358    doc(w, &node.doc);
359    let mut line = String::from("subtype ");
360    line.push_str(node.name.as_str());
361    line.push_str(" is ");
362    line.push_str(&path_text(&node.mark));
363    line.push(';');
364    w.line(&line);
365}
366
367fn render_object(w: &mut Writer, node: &ObjectDecl) {
368    doc(w, &node.doc);
369    let mut line = String::from(node.name.as_str());
370    line.push_str(" : constant ");
371    line.push_str(&path_text(&node.type_));
372    line.push_str(" := ");
373    line.push_str(&expr_text(&node.value));
374    line.push(';');
375    w.line(&line);
376}
377
378fn render_enum_rep(w: &mut Writer, node: &EnumRep) {
379    doc(w, &node.doc);
380    let mut line = String::from("for ");
381    line.push_str(node.type_.as_str());
382    line.push_str(" use (");
383    line.push_str(
384        &node
385            .values
386            .iter()
387            .map(|(literal, code)| {
388                let mut out = String::from(literal.as_str());
389                out.push_str(" => ");
390                out.push_str(&expr_text(code));
391                out
392            })
393            .collect::<Vec<String>>()
394            .join(", "),
395    );
396    line.push_str(");");
397    w.line(&line);
398}
399
400fn subprogram_head(node: &Subprogram) -> String {
401    let mut line = String::from(match node.kind {
402        SubprogramKind::Procedure => "procedure ",
403        SubprogramKind::Function => "function ",
404    });
405    line.push_str(node.name.as_str());
406    if !node.params.is_empty() {
407        line.push_str(" (");
408        line.push_str(
409            &node
410                .params
411                .iter()
412                .map(param_text)
413                .collect::<Vec<String>>()
414                .join("; "),
415        );
416        line.push(')');
417    }
418    if let Some(result) = &node.result {
419        line.push_str(" return ");
420        line.push_str(&path_text(result));
421    }
422    line
423}
424
425fn render_subprogram(w: &mut Writer, node: &Subprogram) {
426    doc(w, &node.doc);
427    let mut line = subprogram_head(node);
428    if node.is_abstract {
429        line.push_str(" is abstract");
430    }
431    line.push(';');
432    w.line(&line);
433}
434
435/// `procedure P (…) is <declarations> begin <statements> end P;`
436///
437/// The leading run of [`Stmt::Var`], [`Stmt::Subtype`] and
438/// [`Stmt::Comment`] goes into the declarative part; everything after it
439/// is the statement sequence, and an empty one is `null;` (RM 5.1 — a
440/// `sequence_of_statements` is never empty).
441fn render_subprogram_body(w: &mut Writer, node: &SubprogramBody) {
442    doc(w, &node.spec.doc);
443    let mut head = subprogram_head(&node.spec);
444    head.push_str(" is");
445    w.line(&head);
446    let split = node
447        .body
448        .iter()
449        .position(|statement| {
450            !matches!(
451                statement,
452                Stmt::Var(_) | Stmt::Subtype { .. } | Stmt::Comment(_)
453            )
454        })
455        .unwrap_or(node.body.len());
456    w.indent();
457    for statement in &node.body[..split] {
458        match statement {
459            Stmt::Var(var) => w.line(&var_text(var)),
460            Stmt::Subtype { name, mark } => w.line(&subtype_text(name, mark)),
461            Stmt::Comment(lines) => doc(w, lines),
462            _ => {}
463        }
464    }
465    w.dedent();
466    w.line("begin");
467    w.indent();
468    if node.body[split..].is_empty() {
469        w.line("null;");
470    } else {
471        render_stmts(w, &node.body[split..]);
472    }
473    w.dedent();
474    let mut tail = String::from("end ");
475    tail.push_str(node.spec.name.as_str());
476    tail.push(';');
477    w.line(&tail);
478}
479
480/// `Name : Mark (Constraint) := Value;`
481fn var_text(node: &VarDecl) -> String {
482    let mut out = String::from(node.name.as_str());
483    out.push_str(" : ");
484    out.push_str(&path_text(&node.mark));
485    if let Some(constraint) = &node.constraint {
486        out.push_str(" (");
487        out.push_str(&expr_text(constraint));
488        out.push(')');
489    }
490    if let Some(value) = &node.value {
491        out.push_str(" := ");
492        out.push_str(&expr_text(value));
493    }
494    out.push(';');
495    out
496}
497
498fn subtype_text(name: &Ident, mark: &Path) -> String {
499    let mut out = String::from("subtype ");
500    out.push_str(name.as_str());
501    out.push_str(" is ");
502    out.push_str(&path_text(mark));
503    out.push(';');
504    out
505}
506
507/// A statement list. A declaration met mid-list opens a `declare` block
508/// that scopes the rest of the list, so everything the walk declared
509/// stays visible to everything after it.
510///
511/// zerodds-lint: recursion-depth = the generated body's block nesting.
512/// The codec walk emits a routine per type rather than inlining one
513/// (`lang-rules/src/emit/encode.rs`), so nothing here follows the type
514/// graph. What does grow with the IDL is the dispatch chain: a union's
515/// cases, an enumeration's enumerators and a `@mutable` aggregate's
516/// members each become one `If` per arm, in the previous arm's `else`.
517/// The model bounds none of the three counts, and Ada nests the arms in
518/// its own output too, so they cannot be walked iteratively the way
519/// `c.rs` walks them. No number is claimed: this signature returns no
520/// error, so nothing here could report one.
521fn render_stmts(w: &mut Writer, nodes: &[Stmt]) {
522    let mut at = 0;
523    while at < nodes.len() {
524        let statement = &nodes[at];
525        if matches!(statement, Stmt::Var(_) | Stmt::Subtype { .. }) {
526            w.line("declare");
527            w.indent();
528            while at < nodes.len() {
529                match &nodes[at] {
530                    Stmt::Var(var) => w.line(&var_text(var)),
531                    Stmt::Subtype { name, mark } => w.line(&subtype_text(name, mark)),
532                    Stmt::Comment(lines) => doc(w, lines),
533                    _ => break,
534                }
535                at += 1;
536            }
537            w.dedent();
538            w.line("begin");
539            w.indent();
540            if nodes[at..].is_empty() {
541                w.line("null;");
542            } else {
543                render_stmts(w, &nodes[at..]);
544            }
545            w.dedent();
546            w.line("end;");
547            return;
548        }
549        render_stmt(w, statement);
550        at += 1;
551    }
552}
553
554/// zerodds-lint: recursion-depth = the generated body's block nesting.
555/// The codec walk emits a routine per type rather than inlining one
556/// (`lang-rules/src/emit/encode.rs`), so nothing here follows the type
557/// graph. What does grow with the IDL is the dispatch chain: a union's
558/// cases, an enumeration's enumerators and a `@mutable` aggregate's
559/// members each become one `If` per arm, in the previous arm's `else`.
560/// The model bounds none of the three counts, and Ada nests the arms in
561/// its own output too, so they cannot be walked iteratively the way
562/// `c.rs` walks them. No number is claimed: this signature returns no
563/// error, so nothing here could report one.
564fn render_stmt(w: &mut Writer, node: &Stmt) {
565    match node {
566        Stmt::Null => w.line("null;"),
567        // Reached only through `render_stmts`, which folds these into a
568        // `declare` block or the subprogram's declarative part.
569        Stmt::Var(var) => w.line(&var_text(var)),
570        Stmt::Subtype { name, mark } => w.line(&subtype_text(name, mark)),
571        Stmt::Assign { place, value } => {
572            let mut line = expr_text(place);
573            line.push_str(" := ");
574            line.push_str(&expr_text(value));
575            line.push(';');
576            w.line(&line);
577        }
578        Stmt::Call(value) => {
579            let mut line = expr_text(value);
580            line.push(';');
581            w.line(&line);
582        }
583        Stmt::Return(value) => match value {
584            Some(value) => {
585                let mut line = String::from("return ");
586                line.push_str(&expr_text(value));
587                line.push(';');
588                w.line(&line);
589            }
590            None => w.line("return;"),
591        },
592        Stmt::If {
593            condition,
594            then,
595            otherwise,
596        } => {
597            let mut head = String::from("if ");
598            head.push_str(&expr_text(condition));
599            head.push_str(" then");
600            w.line(&head);
601            w.indent();
602            if then.is_empty() {
603                w.line("null;");
604            } else {
605                render_stmts(w, then);
606            }
607            w.dedent();
608            if let Some(otherwise) = otherwise {
609                w.line("else");
610                w.indent();
611                if otherwise.is_empty() {
612                    w.line("null;");
613                } else {
614                    render_stmts(w, otherwise);
615                }
616                w.dedent();
617            }
618            w.line("end if;");
619        }
620        Stmt::For {
621            counter,
622            low,
623            high,
624            body,
625        } => {
626            let mut head = String::from("for ");
627            head.push_str(counter.as_str());
628            head.push_str(" in ");
629            head.push_str(&expr_text(low));
630            head.push_str(" .. ");
631            head.push_str(&expr_text(high));
632            head.push_str(" loop");
633            w.line(&head);
634            w.indent();
635            if body.is_empty() {
636                w.line("null;");
637            } else {
638                render_stmts(w, body);
639            }
640            w.dedent();
641            w.line("end loop;");
642        }
643        Stmt::While { condition, body } => {
644            let mut head = String::from("while ");
645            head.push_str(&expr_text(condition));
646            head.push_str(" loop");
647            w.line(&head);
648            w.indent();
649            if body.is_empty() {
650                w.line("null;");
651            } else {
652                render_stmts(w, body);
653            }
654            w.dedent();
655            w.line("end loop;");
656        }
657        Stmt::Exit => w.line("exit;"),
658        Stmt::Declare { vars, body } => {
659            w.line("declare");
660            w.indent();
661            for var in vars {
662                w.line(&var_text(var));
663            }
664            w.dedent();
665            w.line("begin");
666            w.indent();
667            if body.is_empty() {
668                w.line("null;");
669            } else {
670                render_stmts(w, body);
671            }
672            w.dedent();
673            w.line("end;");
674        }
675        Stmt::Comment(lines) => doc(w, lines),
676    }
677}
678
679fn param_text(node: &Param) -> String {
680    let mut out = String::from(node.name.as_str());
681    out.push_str(" : ");
682    // The mode is written even for `in`, which is the default (RM 6.1).
683    // The IDL direction is the one part of an operation's signature that
684    // no other column in this crate can keep, and a spelled-out `in` is
685    // what makes it visible in the generated text.
686    out.push_str(mode_text(node.mode));
687    out.push(' ');
688    out.push_str(&path_text(&node.type_));
689    out
690}
691
692const fn mode_text(mode: Mode) -> &'static str {
693    mode.as_str()
694}
695
696/// zerodds-lint: recursion-depth = the built expression's nesting. A walk
697/// calls a routine per type rather than nesting one, so the one operand
698/// tree that grows with the IDL is an array's default value: one composite
699/// literal per dimension, built in `lang-rules/src/default_ada.rs`, and
700/// nothing bounds the dimension count. No number is claimed: this
701/// signature returns no error.
702fn expr_text(node: &Expr) -> String {
703    match node {
704        Expr::Literal(literal) => literal_text(literal),
705        Expr::Name(name) => path_text(name),
706        Expr::Call { name, args } => {
707            let mut out = path_text(name);
708            // A parameterless call is the bare name: `F ()` is not Ada
709            // (RM 6.4 — an actual_parameter_part is non-empty).
710            if !args.is_empty() {
711                out.push_str(" (");
712                out.push_str(
713                    &args
714                        .iter()
715                        .map(expr_text)
716                        .collect::<Vec<String>>()
717                        .join(", "),
718                );
719                out.push(')');
720            }
721            out
722        }
723        Expr::Negate(inner) => {
724            let mut out = String::from("-");
725            out.push_str(&expr_text(inner));
726            out
727        }
728        Expr::Binary { op, lhs, rhs } => {
729            let mut out = operand_text(lhs);
730            out.push(' ');
731            out.push_str(ada_op(op));
732            out.push(' ');
733            out.push_str(&operand_text(rhs));
734            out
735        }
736        Expr::Unary { op, value } => {
737            let mut out = String::from(ada_op(op));
738            if *op != "-" {
739                out.push(' ');
740            }
741            out.push_str(&operand_text(value));
742            out
743        }
744        Expr::Selected { value, name } => {
745            let mut out = operand_text(value);
746            out.push('.');
747            out.push_str(name.as_str());
748            out
749        }
750        Expr::Indexed { value, index } => {
751            let mut out = operand_text(value);
752            out.push_str(" (");
753            out.push_str(&expr_text(index));
754            out.push(')');
755            out
756        }
757        Expr::Deref(value) => {
758            let mut out = operand_text(value);
759            out.push_str(".all");
760            out
761        }
762        Expr::Slice { value, low, high } => {
763            let mut out = operand_text(value);
764            out.push_str(" (");
765            out.push_str(&expr_text(low));
766            out.push_str(" .. ");
767            out.push_str(&expr_text(high));
768            out.push(')');
769            out
770        }
771        Expr::Attribute { value, name, args } => {
772            let mut out = operand_text(value);
773            out.push('\'');
774            out.push_str(name.as_str());
775            if !args.is_empty() {
776                out.push_str(" (");
777                out.push_str(
778                    &args
779                        .iter()
780                        .map(expr_text)
781                        .collect::<Vec<String>>()
782                        .join(", "),
783                );
784                out.push(')');
785            }
786            out
787        }
788        Expr::Convert { mark, value } => {
789            let mut out = path_text(mark);
790            out.push_str(" (");
791            out.push_str(&expr_text(value));
792            out.push(')');
793            out
794        }
795        Expr::Aggregate(parts) => {
796            if parts.is_empty() {
797                return "(null record)".to_string();
798            }
799            let mut out = String::from("(");
800            out.push_str(
801                &parts
802                    .iter()
803                    .map(|part| {
804                        let mut one = String::from(part.choice.as_str());
805                        one.push_str(" => ");
806                        one.push_str(&expr_text(&part.value));
807                        one
808                    })
809                    .collect::<Vec<String>>()
810                    .join(", "),
811            );
812            out.push(')');
813            out
814        }
815        Expr::BoxDefault => "<>".to_string(),
816        Expr::New { mark, init } => {
817            let mut out = String::from("new ");
818            out.push_str(&path_text(mark));
819            match init {
820                AllocInit::Range { low, high } => {
821                    out.push_str(" (");
822                    out.push_str(&expr_text(low));
823                    out.push_str(" .. ");
824                    out.push_str(&expr_text(high));
825                    out.push(')');
826                }
827                AllocInit::Qualified(value) => {
828                    out.push_str("'(");
829                    out.push_str(&expr_text(value));
830                    out.push(')');
831                }
832            }
833            out
834        }
835    }
836}
837
838/// An operand, parenthesised where its own operator could re-associate.
839///
840/// Blanket rather than minimal: Ada refuses `A and then B or else C`
841/// outright (RM 4.4 puts one `logical_operator` class per expression),
842/// and a spelled-out parenthesis is never wrong.
843///
844/// zerodds-lint: recursion-depth = the built expression's nesting. A walk
845/// calls a routine per type rather than nesting one, so the one operand
846/// tree that grows with the IDL is an array's default value: one composite
847/// literal per dimension, built in `lang-rules/src/default_ada.rs`, and
848/// nothing bounds the dimension count. No number is claimed: this
849/// signature returns no error.
850fn operand_text(node: &Expr) -> String {
851    match node {
852        Expr::Binary { .. } | Expr::Unary { .. } | Expr::Negate(_) => {
853            let mut out = String::from("(");
854            out.push_str(&expr_text(node));
855            out.push(')');
856            out
857        }
858        _ => expr_text(node),
859    }
860}
861
862/// The Ada spelling of the shared walk's operator.
863fn ada_op(op: &str) -> &str {
864    match op {
865        "==" => "=",
866        "!=" => "/=",
867        "&&" => "and then",
868        "||" => "or else",
869        "&" => "and",
870        "|" => "or",
871        "^" => "xor",
872        "%" => "mod",
873        "!" | "~" => "not",
874        other => other,
875    }
876}
877
878fn literal_text(literal: &Literal) -> String {
879    match literal {
880        Literal::Int(text) | Literal::Float(text) => text.clone(),
881        Literal::Bool(value) => {
882            if *value {
883                "True".to_string()
884            } else {
885                "False".to_string()
886            }
887        }
888        // The code point as a number, not `'x'`. An Ada character
889        // literal is a value of `Standard.Character`, which is Latin-1
890        // (RM A.1); an IDL `char` is one octet and an IDL `wchar` two,
891        // and this column holds both as `Interfaces` integers. Writing
892        // the number is what keeps the constant's type the member's.
893        Literal::Char(ch) => alloc::format!("{}", u32::from(*ch)),
894        Literal::Str(text) => ada_string(text),
895        // A positional array aggregate. Ada writes a hexadecimal literal
896        // as `16#FF#` (RM 2.4.2) — there is no `0x`.
897        Literal::Bytes(bytes) => {
898            let mut out = String::from("(");
899            let parts: Vec<String> = bytes
900                .iter()
901                .map(|b| alloc::format!("16#{b:02X}#"))
902                .collect();
903            out.push_str(&parts.join(", "));
904            out.push(')');
905            out
906        }
907        Literal::Null => "null".to_string(),
908    }
909}
910
911/// An Ada string literal.
912///
913/// RM 2.6: a quotation mark inside a string literal is written twice.
914/// There is no backslash escape in Ada at all, so
915/// [`crate::writer::escape_string`] would put a graphic character into
916/// the literal rather than an escape — a `\n` in an IDL constant would
917/// arrive in the generated spec as a backslash and an `n`.
918///
919/// Characters an Ada string literal cannot hold — a line break, a
920/// control character — are refused by `zerodds_lang_rules::adapter::ada`
921/// before they reach here, because there is no spelling for them and
922/// dropping one would change the constant.
923fn ada_string(text: &str) -> String {
924    let mut out = String::from("\"");
925    for ch in text.chars() {
926        if ch == '"' {
927            out.push('"');
928        }
929        out.push(ch);
930    }
931    out.push('"');
932    out
933}
934
935fn doc(w: &mut Writer, doc: &Doc) {
936    if doc.is_empty() {
937        return;
938    }
939    comment_lines(w, "--  ", doc.as_lines());
940}
941
942#[cfg(test)]
943mod tests {
944    #![allow(clippy::expect_used, clippy::panic, clippy::unwrap_used)]
945    use super::*;
946    use alloc::vec;
947    use zerodds_lang_cst::ada::{Primitive, Record};
948
949    fn component(name: &str, type_: Primitive) -> Component {
950        Component {
951            doc: Doc::none(),
952            name: Ident::new(name),
953            type_: type_.path(),
954        }
955    }
956
957    #[test]
958    fn a_record_writes_one_component_per_line() {
959        let node = Decl::Type(TypeDecl {
960            doc: Doc::none(),
961            name: Ident::new("Pose"),
962            def: TypeDef::Record(Record::plain(vec![
963                component("X", Primitive::Integer32),
964                component("Y", Primitive::Float64),
965            ])),
966        });
967        assert_eq!(
968            decl(&node),
969            "type Pose is record\n   \
970             X : Interfaces.Integer_32;\n   \
971             Y : Interfaces.IEEE_Float_64;\n\
972             end record;\n"
973        );
974    }
975
976    #[test]
977    fn a_record_with_no_components_is_a_null_record() {
978        // RM 3.8: `record end record` has an empty `component_list`,
979        // which is not Ada. The corpus contains aggregates with no
980        // members, so this arm is reached.
981        let node = Decl::Type(TypeDecl {
982            doc: Doc::none(),
983            name: Ident::new("Empty"),
984            def: TypeDef::Record(Record::plain(Vec::new())),
985        });
986        assert_eq!(decl(&node), "type Empty is null record;\n");
987    }
988
989    #[test]
990    fn a_discriminated_record_writes_its_variant_part() {
991        let node = Decl::Type(TypeDecl {
992            doc: Doc::none(),
993            name: Ident::new("Payload"),
994            def: TypeDef::Record(Record {
995                body_text: zerodds_lang_cst::common::BodyText::none(),
996                discriminants: vec![Discriminant {
997                    name: Ident::new("D"),
998                    type_: Path::single("Mode"),
999                    default: Some(Expr::name("IDLE")),
1000                }],
1001                components: Vec::new(),
1002                variant: Some(Variant {
1003                    discriminant: Ident::new("D"),
1004                    arms: vec![
1005                        Arm {
1006                            choices: vec![Choice::Value(Expr::name("IDLE"))],
1007                            components: vec![component("Idle", Primitive::Integer32)],
1008                        },
1009                        Arm {
1010                            choices: vec![Choice::Others],
1011                            components: Vec::new(),
1012                        },
1013                    ],
1014                }),
1015            }),
1016        });
1017        assert_eq!(
1018            decl(&node),
1019            "type Payload (D : Mode := IDLE) is record\n   \
1020             case D is\n      \
1021             when IDLE =>\n         \
1022             Idle : Interfaces.Integer_32;\n      \
1023             when others =>\n         \
1024             null;\n   \
1025             end case;\n\
1026             end record;\n"
1027        );
1028    }
1029
1030    #[test]
1031    fn an_enumeration_and_its_representation_clause_are_two_declarations() {
1032        // RM 3.5.1 numbers the literals 0, 1, 2 … in declaration order;
1033        // the clause of RM 13.4 is the only thing that carries the IDL
1034        // `@value` gaps into the generated spec.
1035        let type_ = Decl::Type(TypeDecl {
1036            doc: Doc::none(),
1037            name: Ident::new("Mode"),
1038            def: TypeDef::Enumeration(vec![Ident::new("IDLE"), Ident::new("ACTIVE")]),
1039        });
1040        let rep = Decl::EnumRep(EnumRep {
1041            doc: Doc::none(),
1042            type_: Ident::new("Mode"),
1043            values: vec![
1044                (Ident::new("IDLE"), Expr::int(0)),
1045                (Ident::new("ACTIVE"), Expr::int(4)),
1046            ],
1047        });
1048        assert_eq!(decl(&type_), "type Mode is (IDLE, ACTIVE);\n");
1049        assert_eq!(decl(&rep), "for Mode use (IDLE => 0, ACTIVE => 4);\n");
1050    }
1051
1052    #[test]
1053    fn an_unconstrained_array_and_the_access_type_that_reaches_it() {
1054        let array = Decl::Type(TypeDecl {
1055            doc: Doc::none(),
1056            name: Ident::new("Zd_Seq_Long_Array"),
1057            def: TypeDef::Array {
1058                index: vec![Index::Unconstrained(Path::single("Natural"))],
1059                element: Primitive::Integer32.path(),
1060            },
1061        });
1062        let access = Decl::Type(TypeDecl {
1063            doc: Doc::none(),
1064            name: Ident::new("Zd_Seq_Long"),
1065            def: TypeDef::Access(Path::single("Zd_Seq_Long_Array")),
1066        });
1067        assert_eq!(
1068            decl(&array),
1069            "type Zd_Seq_Long_Array is array (Natural range <>) of Interfaces.Integer_32;\n"
1070        );
1071        assert_eq!(
1072            decl(&access),
1073            "type Zd_Seq_Long is access Zd_Seq_Long_Array;\n"
1074        );
1075    }
1076
1077    #[test]
1078    fn a_constrained_array_writes_its_bounds() {
1079        let node = Decl::Type(TypeDecl {
1080            doc: Doc::none(),
1081            name: Ident::new("Zd_Arr_Long_2"),
1082            def: TypeDef::Array {
1083                index: vec![Index::Range {
1084                    low: Expr::int(0),
1085                    high: Expr::int(1),
1086                }],
1087                element: Primitive::Integer32.path(),
1088            },
1089        });
1090        assert_eq!(
1091            decl(&node),
1092            "type Zd_Arr_Long_2 is array (0 .. 1) of Interfaces.Integer_32;\n"
1093        );
1094    }
1095
1096    #[test]
1097    fn an_incomplete_type_declares_a_name_and_nothing_else() {
1098        // RM 3.10.1. The completion is a later declaration; between the
1099        // two the name may only be the designated subtype of an access
1100        // type.
1101        let incomplete = Decl::Type(TypeDecl {
1102            doc: Doc::none(),
1103            name: Ident::new("Node"),
1104            def: TypeDef::Incomplete,
1105        });
1106        let access = Decl::Type(TypeDecl {
1107            doc: Doc::none(),
1108            name: Ident::new("Node_Array"),
1109            def: TypeDef::Access(Path::single("Node")),
1110        });
1111        assert_eq!(decl(&incomplete), "type Node;\n");
1112        assert_eq!(decl(&access), "type Node_Array is access Node;\n");
1113    }
1114
1115    #[test]
1116    fn a_subtype_is_not_a_derived_type() {
1117        // RM 3.2.2 against RM 3.4: the first is another name for one
1118        // type, the second is a new type with its own identity.
1119        let alias = Decl::Subtype(SubtypeDecl {
1120            doc: Doc::none(),
1121            name: Ident::new("Distance"),
1122            mark: Primitive::Integer32.path(),
1123        });
1124        let derived = Decl::Type(TypeDecl {
1125            doc: Doc::none(),
1126            name: Ident::new("Flags"),
1127            def: TypeDef::Derived(Primitive::Unsigned16.path()),
1128        });
1129        assert_eq!(decl(&alias), "subtype Distance is Interfaces.Integer_32;\n");
1130        assert_eq!(
1131            decl(&derived),
1132            "type Flags is new Interfaces.Unsigned_16;\n"
1133        );
1134    }
1135
1136    #[test]
1137    fn a_constant_names_its_type_because_ada_has_no_untyped_one() {
1138        let node = Decl::Object(ObjectDecl {
1139            doc: Doc::none(),
1140            name: Ident::new("LIMIT"),
1141            type_: Primitive::Unsigned8.path(),
1142            value: Expr::int(8),
1143        });
1144        assert_eq!(
1145            decl(&node),
1146            "LIMIT : constant Interfaces.Unsigned_8 := 8;\n"
1147        );
1148    }
1149
1150    #[test]
1151    fn an_abstract_subprogram_writes_its_modes() {
1152        let procedure = Decl::Subprogram(Subprogram {
1153            doc: Doc::none(),
1154            kind: SubprogramKind::Procedure,
1155            name: Ident::new("Go"),
1156            params: vec![
1157                Param {
1158                    name: Ident::new("Self"),
1159                    mode: Mode::In,
1160                    type_: Path::single("Control"),
1161                },
1162                Param {
1163                    name: Ident::new("Target"),
1164                    mode: Mode::Out,
1165                    type_: Path::single("Pose"),
1166                },
1167            ],
1168            result: None,
1169            is_abstract: true,
1170        });
1171        let function = Decl::Subprogram(Subprogram {
1172            doc: Doc::none(),
1173            kind: SubprogramKind::Function,
1174            name: Ident::new("Status"),
1175            params: vec![Param {
1176                name: Ident::new("Self"),
1177                mode: Mode::In,
1178                type_: Path::single("Control"),
1179            }],
1180            result: Some(Primitive::Integer32.path()),
1181            is_abstract: true,
1182        });
1183        assert_eq!(
1184            decl(&procedure),
1185            "procedure Go (Self : in Control; Target : out Pose) is abstract;\n"
1186        );
1187        assert_eq!(
1188            decl(&function),
1189            "function Status (Self : in Control) return Interfaces.Integer_32 is abstract;\n"
1190        );
1191    }
1192
1193    #[test]
1194    fn a_string_literal_doubles_its_quote_and_never_escapes_it() {
1195        // RM 2.6. A backslash is an ordinary graphic character in Ada,
1196        // so the C-family escape would land in the value.
1197        assert_eq!(
1198            expr(&Expr::Literal(Literal::Str("a\"b".to_string()))),
1199            "\"a\"\"b\""
1200        );
1201        assert_eq!(
1202            expr(&Expr::Literal(Literal::Str("a\\b".to_string()))),
1203            "\"a\\b\""
1204        );
1205    }
1206
1207    #[test]
1208    fn a_string_constant_is_a_call_because_a_private_type_has_no_literal() {
1209        let node = Decl::Object(ObjectDecl {
1210            doc: Doc::none(),
1211            name: Ident::new("TAG"),
1212            type_: Primitive::UnboundedString.path(),
1213            value: Expr::Call {
1214                name: Path::new(&["Ada", "Strings", "Unbounded", "To_Unbounded_String"]),
1215                args: vec![Expr::Literal(Literal::Str("abc".to_string()))],
1216            },
1217        });
1218        assert_eq!(
1219            decl(&node),
1220            "TAG : constant Ada.Strings.Unbounded.Unbounded_String := \
1221             Ada.Strings.Unbounded.To_Unbounded_String (\"abc\");\n"
1222        );
1223    }
1224
1225    #[test]
1226    fn a_file_writes_its_context_clause_before_the_package() {
1227        // RM 10.1.2: a `with` clause belongs to the compilation unit and
1228        // comes before it, not inside it.
1229        let node = File {
1230            doc: Doc::line("Generated. Do not edit."),
1231            withs: vec![Path::single("Interfaces")],
1232            use_types: Vec::new(),
1233            package: Ident::new("Zd_Types"),
1234            decls: vec![Decl::Type(TypeDecl {
1235                doc: Doc::none(),
1236                name: Ident::new("Pose"),
1237                def: TypeDef::Record(Record::plain(vec![component("X", Primitive::Integer32)])),
1238            })],
1239        };
1240        assert_eq!(
1241            file(&node),
1242            "--  Generated. Do not edit.\n\
1243             \n\
1244             with Interfaces;\n\
1245             \n\
1246             package Zd_Types is\n\
1247             \n   \
1248             type Pose is record\n      \
1249             X : Interfaces.Integer_32;\n   \
1250             end record;\n\
1251             \n\
1252             end Zd_Types;\n"
1253        );
1254    }
1255
1256    #[test]
1257    fn a_use_type_clause_stands_after_the_with_it_depends_on() {
1258        // RM 10.1.2 makes both context items; RM 8.4 needs the subtype
1259        // mark visible, and it is the `with` that makes it so.
1260        let node = File {
1261            doc: Doc::none(),
1262            withs: vec![Path::single("Interfaces")],
1263            use_types: vec![Primitive::Integer16.path()],
1264            package: Ident::new("Zd_Types"),
1265            decls: Vec::new(),
1266        };
1267        assert_eq!(
1268            file(&node),
1269            "with Interfaces;\n\
1270             use type Interfaces.Integer_16;\n\
1271             \n\
1272             package Zd_Types is\n\
1273             \n\
1274             end Zd_Types;\n"
1275        );
1276    }
1277
1278    #[test]
1279    fn a_boolean_is_written_with_the_case_ada_declares_it_in() {
1280        // RM A.1 declares `Boolean` as `(False, True)`. The identifiers
1281        // fold, so the case is convention — but a file that wrote `true`
1282        // would differ from every Ada source beside it.
1283        assert_eq!(expr(&Expr::Literal(Literal::Bool(true))), "True");
1284        assert_eq!(expr(&Expr::Literal(Literal::Bool(false))), "False");
1285    }
1286
1287    #[test]
1288    fn a_hexadecimal_literal_uses_the_ada_base_form() {
1289        // RM 2.4.2: `16#FF#`, never `0xFF`.
1290        assert_eq!(
1291            expr(&Expr::Literal(Literal::Bytes(alloc::vec![1, 255]))),
1292            "(16#01#, 16#FF#)"
1293        );
1294    }
1295
1296    #[test]
1297    fn a_comment_uses_the_ada_marker_and_has_no_closing_delimiter() {
1298        // RM 2.7.
1299        let node = Decl::Comment(Doc::lines(&["first", "second"]));
1300        assert_eq!(decl(&node), "--  first\n--  second\n");
1301    }
1302
1303    use zerodds_lang_cst::ada::{AllocInit, Association, Stmt, SubprogramBody, VarDecl};
1304
1305    fn name(text: &str) -> Expr {
1306        Expr::name(text)
1307    }
1308
1309    #[test]
1310    fn the_walks_operators_take_their_ada_spellings() {
1311        let node = Expr::Binary {
1312            op: "&&",
1313            lhs: alloc::boxed::Box::new(Expr::Binary {
1314                op: "==",
1315                lhs: alloc::boxed::Box::new(name("A")),
1316                rhs: alloc::boxed::Box::new(name("B")),
1317            }),
1318            rhs: alloc::boxed::Box::new(Expr::Unary {
1319                op: "!",
1320                value: alloc::boxed::Box::new(name("C")),
1321            }),
1322        };
1323        // Both operands parenthesised: Ada refuses a mixed logical
1324        // expression without them (RM 4.4), and blanket parentheses are
1325        // never wrong.
1326        assert_eq!(expr(&node), "(A = B) and then (not C)");
1327    }
1328
1329    #[test]
1330    fn selection_indexing_dereference_and_slice_render_as_ada_writes_them() {
1331        let selected = Expr::Selected {
1332            value: alloc::boxed::Box::new(name("v")),
1333            name: Ident::new("Key"),
1334        };
1335        assert_eq!(expr(&selected), "v.Key");
1336        let indexed = Expr::Indexed {
1337            value: alloc::boxed::Box::new(name("v")),
1338            index: alloc::boxed::Box::new(name("zd_i")),
1339        };
1340        assert_eq!(expr(&indexed), "v (zd_i)");
1341        let deref = Expr::Deref(alloc::boxed::Box::new(name("v")));
1342        assert_eq!(expr(&deref), "v.all");
1343        let slice = Expr::Slice {
1344            value: alloc::boxed::Box::new(deref),
1345            low: alloc::boxed::Box::new(Expr::int(0)),
1346            high: alloc::boxed::Box::new(name("zd_n")),
1347        };
1348        assert_eq!(expr(&slice), "v.all (0 .. zd_n)");
1349    }
1350
1351    #[test]
1352    fn an_attribute_a_conversion_and_an_aggregate_render() {
1353        let length = Expr::Attribute {
1354            value: alloc::boxed::Box::new(Expr::Deref(alloc::boxed::Box::new(name("v")))),
1355            name: Ident::new("Length"),
1356            args: Vec::new(),
1357        };
1358        assert_eq!(expr(&length), "v.all'Length");
1359        let enum_val = Expr::Attribute {
1360            value: alloc::boxed::Box::new(name("Colour")),
1361            name: Ident::new("Enum_Val"),
1362            args: vec![Expr::int(4)],
1363        };
1364        assert_eq!(expr(&enum_val), "Colour'Enum_Val (4)");
1365        let convert = Expr::Convert {
1366            mark: Primitive::Unsigned32.path(),
1367            value: alloc::boxed::Box::new(length),
1368        };
1369        assert_eq!(expr(&convert), "Interfaces.Unsigned_32 (v.all'Length)");
1370        let aggregate = Expr::Aggregate(vec![
1371            Association {
1372                choice: Ident::new("Present"),
1373                value: Expr::Literal(Literal::Bool(true)),
1374            },
1375            Association {
1376                choice: Ident::new("Value"),
1377                value: Expr::int(7),
1378            },
1379        ]);
1380        assert_eq!(expr(&aggregate), "(Present => True, Value => 7)");
1381        let boxed = Expr::Aggregate(vec![Association {
1382            choice: Ident::new("others"),
1383            value: Expr::BoxDefault,
1384        }]);
1385        assert_eq!(expr(&boxed), "(others => <>)");
1386    }
1387
1388    #[test]
1389    fn an_allocator_takes_a_range_or_a_qualified_value() {
1390        let empty = Expr::New {
1391            mark: Path::single("Zd_Seq_Long_Array"),
1392            init: AllocInit::Range {
1393                low: alloc::boxed::Box::new(Expr::int(0)),
1394                high: alloc::boxed::Box::new(Expr::int(-1)),
1395            },
1396        };
1397        assert_eq!(expr(&empty), "new Zd_Seq_Long_Array (0 .. -1)");
1398        let kept = Expr::New {
1399            mark: Path::single("Zd_Seq_Long_Array"),
1400            init: AllocInit::Qualified(alloc::boxed::Box::new(name("v"))),
1401        };
1402        assert_eq!(expr(&kept), "new Zd_Seq_Long_Array'(v)");
1403    }
1404
1405    #[test]
1406    fn a_declaration_mid_list_opens_a_declare_block_over_the_rest() {
1407        // RM 3.11 keeps declarations out of a statement sequence; the
1408        // fold is what keeps the walk's declared local visible to every
1409        // statement after it.
1410        let nodes = vec![
1411            Stmt::Assign {
1412                place: name("a"),
1413                value: Expr::int(1),
1414            },
1415            Stmt::Var(VarDecl {
1416                name: Ident::new("zd_n"),
1417                mark: Primitive::Unsigned32.path(),
1418                constraint: None,
1419                value: Some(Expr::int(0)),
1420            }),
1421            Stmt::Assign {
1422                place: name("a"),
1423                value: name("zd_n"),
1424            },
1425        ];
1426        assert_eq!(
1427            stmts(&nodes),
1428            "a := 1;\n\
1429             declare\n   \
1430             zd_n : Interfaces.Unsigned_32 := 0;\n\
1431             begin\n   \
1432             a := zd_n;\n\
1433             end;\n"
1434        );
1435    }
1436
1437    #[test]
1438    fn a_subprogram_body_hoists_its_leading_declarations() {
1439        let node = Decl::SubprogramBody(SubprogramBody {
1440            spec: Subprogram {
1441                doc: Doc::none(),
1442                kind: SubprogramKind::Function,
1443                name: Ident::new("zd_dec_X"),
1444                params: vec![Param {
1445                    name: Ident::new("r"),
1446                    mode: Mode::InOut,
1447                    type_: Path::new(&["Zd_Wire", "Reader"]),
1448                }],
1449                result: Some(Path::single("X")),
1450                is_abstract: false,
1451            },
1452            body: vec![
1453                Stmt::Var(VarDecl {
1454                    name: Ident::new("v"),
1455                    mark: Path::single("X"),
1456                    constraint: None,
1457                    value: None,
1458                }),
1459                Stmt::Return(Some(name("v"))),
1460            ],
1461        });
1462        assert_eq!(
1463            decl(&node),
1464            "function zd_dec_X (r : in out Zd_Wire.Reader) return X is\n   \
1465             v : X;\n\
1466             begin\n   \
1467             return v;\n\
1468             end zd_dec_X;\n"
1469        );
1470    }
1471
1472    #[test]
1473    fn a_constrained_object_declaration_carries_its_discriminant() {
1474        // RM 3.7.1: the one way a body obtains a value whose
1475        // discriminant it only knows at run time.
1476        let node = Stmt::Declare {
1477            vars: vec![VarDecl {
1478                name: Ident::new("zd_nd"),
1479                mark: Path::single("Zd_Sample"),
1480                constraint: Some(name("zd_d")),
1481                value: None,
1482            }],
1483            body: vec![Stmt::Assign {
1484                place: name("v"),
1485                value: name("zd_nd"),
1486            }],
1487        };
1488        assert_eq!(
1489            stmt(&node),
1490            "declare\n   \
1491             zd_nd : Zd_Sample (zd_d);\n\
1492             begin\n   \
1493             v := zd_nd;\n\
1494             end;\n"
1495        );
1496    }
1497
1498    #[test]
1499    fn the_loop_forms_and_exit_render() {
1500        let counted = Stmt::For {
1501            counter: Ident::new("zd_i"),
1502            low: Expr::int(0),
1503            high: Expr::int(2),
1504            body: vec![Stmt::Null],
1505        };
1506        assert_eq!(
1507            stmt(&counted),
1508            "for zd_i in 0 .. 2 loop\n   null;\nend loop;\n"
1509        );
1510        let headed = Stmt::While {
1511            condition: Expr::Literal(Literal::Bool(true)),
1512            body: vec![Stmt::Exit],
1513        };
1514        assert_eq!(stmt(&headed), "while True loop\n   exit;\nend loop;\n");
1515    }
1516
1517    #[test]
1518    fn an_if_writes_its_else_and_an_empty_arm_is_null() {
1519        let node = Stmt::If {
1520            condition: name("ok"),
1521            then: Vec::new(),
1522            otherwise: Some(vec![Stmt::Return(None)]),
1523        };
1524        assert_eq!(
1525            stmt(&node),
1526            "if ok then\n   null;\nelse\n   return;\nend if;\n"
1527        );
1528    }
1529}