Skip to main content

zerodds_lang_render/
ada83.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2026 ZeroDDS Contributors
3//! Rendering the Ada 83 CST. Spec §5.
4//!
5//! Line for line the same style `zerodds_lang_render::ada` writes —
6//! three spaces per level, `--` comments, `Mixed_Case_With_Underscores`
7//! — because RM83 fixes none of that either, and there is nothing in
8//! this crate's Ada 83 support that departs from the convention its
9//! Ada 2012 sibling already follows.
10//!
11//! What differs is only what `zerodds_lang_cst::ada83` has no node for:
12//! no `use type` clause line (see the module documentation there), no
13//! enumeration type and no representation clause, no `is abstract`.
14//! Everything else below is the identical rendering of an identically
15//! shaped node.
16
17use crate::writer::{Writer, comment_lines, verbatim_lines};
18use alloc::string::{String, ToString};
19use alloc::vec::Vec;
20use zerodds_lang_cst::ada83::{
21    AllocInit, Arm, Choice, Component, Decl, Discriminant, Expr, File, Index, Mode, ObjectDecl,
22    Param, Stmt, Subprogram, SubprogramBody, SubprogramKind, SubtypeDecl, TypeDecl, TypeDef,
23    VarDecl, Variant,
24};
25use zerodds_lang_cst::common::{Doc, Ident, Literal, Path};
26
27/// The indentation one level costs.
28const UNIT: &str = "   ";
29
30/// Render a whole package specification.
31#[must_use]
32pub fn file(file: &File) -> String {
33    let mut w = Writer::new(UNIT);
34    doc(&mut w, &file.doc);
35    if !file.withs.is_empty() {
36        if !file.doc.is_empty() {
37            w.blank_line();
38        }
39        for unit in &file.withs {
40            let mut line = String::from("with ");
41            line.push_str(&path_text(unit));
42            line.push(';');
43            w.line(&line);
44        }
45    }
46    w.blank_line();
47    let mut head = String::from("package ");
48    head.push_str(file.package.as_str());
49    head.push_str(" is");
50    w.line(&head);
51    w.indent();
52    for decl in &file.decls {
53        w.blank_line();
54        render_decl(&mut w, decl);
55    }
56    w.dedent();
57    w.blank_line();
58    let mut tail = String::from("end ");
59    tail.push_str(file.package.as_str());
60    tail.push(';');
61    w.line(&tail);
62    w.finish()
63}
64
65/// Render a package body: `package body Name is … end Name;`.
66///
67/// The second half of what a generated Ada unit is. A subprogram body
68/// is not a declaration a package *specification* may carry (RM 7.1
69/// admits `basic_declarative_item`s, and a `body` is not one), so the
70/// codec's bodies stand here and the profiles that name them stand in
71/// the spec. The `with` clauses are the spec's: RM 10.1.2 extends a
72/// context clause of a library unit declaration over the corresponding
73/// body, so a body that repeated them would say the same thing twice.
74#[must_use]
75pub fn package_body(package: &Ident, decls: &[Decl]) -> String {
76    let mut w = Writer::new(UNIT);
77    let mut head = String::from("package body ");
78    head.push_str(package.as_str());
79    head.push_str(" is");
80    w.line(&head);
81    w.indent();
82    for decl in decls {
83        w.blank_line();
84        render_decl(&mut w, decl);
85    }
86    w.dedent();
87    w.blank_line();
88    let mut tail = String::from("end ");
89    tail.push_str(package.as_str());
90    tail.push(';');
91    w.line(&tail);
92    w.finish()
93}
94
95/// Render one declaration, for a caller that has only one.
96#[must_use]
97pub fn decl(node: &Decl) -> String {
98    let mut w = Writer::new(UNIT);
99    render_decl(&mut w, node);
100    w.finish()
101}
102
103/// Render one expression, for a caller assembling one.
104#[must_use]
105pub fn expr(node: &Expr) -> String {
106    expr_text(node)
107}
108
109/// The identifier a name renders as, for a caller assembling one.
110#[must_use]
111pub fn ident(name: &Ident) -> String {
112    name.as_str().to_string()
113}
114
115/// A qualified name, joined with the selector Ada uses.
116#[must_use]
117pub fn path(name: &Path) -> String {
118    path_text(name)
119}
120
121fn path_text(name: &Path) -> String {
122    name.segments()
123        .iter()
124        .map(|segment| segment.as_str())
125        .collect::<Vec<&str>>()
126        .join(".")
127}
128
129/// Render one statement, for a test that holds only one.
130#[must_use]
131pub fn stmt(node: &Stmt) -> String {
132    let mut w = Writer::new(UNIT);
133    render_stmts(&mut w, core::slice::from_ref(node));
134    w.finish()
135}
136
137/// Render a statement list, `declare` blocks folded in.
138#[must_use]
139pub fn stmts(nodes: &[Stmt]) -> String {
140    let mut w = Writer::new(UNIT);
141    render_stmts(&mut w, nodes);
142    w.finish()
143}
144
145fn render_decl(w: &mut Writer, node: &Decl) {
146    match node {
147        Decl::Type(node) => render_type_decl(w, node),
148        Decl::Subtype(node) => render_subtype(w, node),
149        Decl::Object(node) => render_object(w, node),
150        Decl::Subprogram(node) => render_subprogram(w, node),
151        Decl::SubprogramBody(node) => render_subprogram_body(w, node),
152        Decl::Comment(lines) => doc(w, lines),
153        Decl::Verbatim(text) => verbatim_lines(w, text),
154    }
155}
156
157fn render_type_decl(w: &mut Writer, node: &TypeDecl) {
158    doc(w, &node.doc);
159    let mut head = String::from("type ");
160    head.push_str(node.name.as_str());
161    match &node.def {
162        TypeDef::Record(record) => {
163            head.push_str(&discriminant_text(&record.discriminants));
164            if record.is_null() {
165                head.push_str(" is null record;");
166                w.line(&head);
167                return;
168            }
169            head.push_str(" is record");
170            w.line(&head);
171            w.indent();
172            for text in &record.body_text.begin {
173                verbatim_lines(w, text);
174            }
175            for component in &record.components {
176                render_component(w, component);
177            }
178            if let Some(variant) = &record.variant {
179                render_variant(w, variant);
180            }
181            for text in &record.body_text.end {
182                verbatim_lines(w, text);
183            }
184            w.dedent();
185            w.line("end record;");
186        }
187        TypeDef::Derived(mark) => {
188            head.push_str(" is new ");
189            head.push_str(&path_text(mark));
190            head.push(';');
191            w.line(&head);
192        }
193        TypeDef::Array { index, element } => {
194            head.push_str(" is array (");
195            head.push_str(
196                &index
197                    .iter()
198                    .map(index_text)
199                    .collect::<Vec<String>>()
200                    .join(", "),
201            );
202            head.push_str(") of ");
203            head.push_str(&path_text(element));
204            head.push(';');
205            w.line(&head);
206        }
207        TypeDef::Access(designated) => {
208            head.push_str(" is access ");
209            head.push_str(&path_text(designated));
210            head.push(';');
211            w.line(&head);
212        }
213        // RM83 3.10.1: the name and nothing else.
214        TypeDef::Incomplete => {
215            head.push(';');
216            w.line(&head);
217        }
218    }
219}
220
221/// `(D : Long_Integer := 0)`, or nothing.
222fn discriminant_text(discriminants: &[Discriminant]) -> String {
223    if discriminants.is_empty() {
224        return String::new();
225    }
226    let parts: Vec<String> = discriminants
227        .iter()
228        .map(|discriminant| {
229            let mut out = String::from(discriminant.name.as_str());
230            out.push_str(" : ");
231            out.push_str(&path_text(&discriminant.type_));
232            if let Some(default) = &discriminant.default {
233                out.push_str(" := ");
234                out.push_str(&expr_text(default));
235            }
236            out
237        })
238        .collect();
239    let mut out = String::from(" (");
240    out.push_str(&parts.join("; "));
241    out.push(')');
242    out
243}
244
245fn render_component(w: &mut Writer, node: &Component) {
246    doc(w, &node.doc);
247    let mut line = String::from(node.name.as_str());
248    line.push_str(" : ");
249    line.push_str(&path_text(&node.type_));
250    line.push(';');
251    w.line(&line);
252}
253
254/// `case D is … end case;` (RM83 3.7.3).
255fn render_variant(w: &mut Writer, node: &Variant) {
256    let mut head = String::from("case ");
257    head.push_str(node.discriminant.as_str());
258    head.push_str(" is");
259    w.line(&head);
260    w.indent();
261    for arm in &node.arms {
262        render_arm(w, arm);
263    }
264    w.dedent();
265    w.line("end case;");
266}
267
268fn render_arm(w: &mut Writer, node: &Arm) {
269    let mut head = String::from("when ");
270    head.push_str(
271        &node
272            .choices
273            .iter()
274            .map(choice_text)
275            .collect::<Vec<String>>()
276            .join(" | "),
277    );
278    head.push_str(" =>");
279    w.line(&head);
280    w.indent();
281    if node.components.is_empty() {
282        w.line("null;");
283    } else {
284        for component in &node.components {
285            render_component(w, component);
286        }
287    }
288    w.dedent();
289}
290
291fn choice_text(node: &Choice) -> String {
292    match node {
293        Choice::Value(value) => expr_text(value),
294        Choice::Others => "others".to_string(),
295    }
296}
297
298fn index_text(node: &Index) -> String {
299    match node {
300        Index::Range { low, high } => {
301            let mut out = expr_text(low);
302            out.push_str(" .. ");
303            out.push_str(&expr_text(high));
304            out
305        }
306        Index::Unconstrained(mark) => {
307            let mut out = path_text(mark);
308            out.push_str(" range <>");
309            out
310        }
311    }
312}
313
314fn render_subtype(w: &mut Writer, node: &SubtypeDecl) {
315    doc(w, &node.doc);
316    let mut line = String::from("subtype ");
317    line.push_str(node.name.as_str());
318    line.push_str(" is ");
319    line.push_str(&path_text(&node.mark));
320    line.push(';');
321    w.line(&line);
322}
323
324fn render_object(w: &mut Writer, node: &ObjectDecl) {
325    doc(w, &node.doc);
326    let mut line = String::from(node.name.as_str());
327    line.push_str(" : constant ");
328    line.push_str(&path_text(&node.type_));
329    line.push_str(" := ");
330    line.push_str(&expr_text(&node.value));
331    line.push(';');
332    w.line(&line);
333}
334
335fn subprogram_head(
336    kind: SubprogramKind,
337    name: &Ident,
338    params: &[Param],
339    result: &Option<Path>,
340) -> String {
341    let mut line = String::from(match kind {
342        SubprogramKind::Procedure => "procedure ",
343        SubprogramKind::Function => "function ",
344    });
345    line.push_str(name.as_str());
346    if !params.is_empty() {
347        line.push_str(" (");
348        line.push_str(
349            &params
350                .iter()
351                .map(param_text)
352                .collect::<Vec<String>>()
353                .join("; "),
354        );
355        line.push(')');
356    }
357    if let Some(result) = result {
358        line.push_str(" return ");
359        line.push_str(&path_text(result));
360    }
361    line
362}
363
364fn render_subprogram(w: &mut Writer, node: &Subprogram) {
365    doc(w, &node.doc);
366    let mut line = subprogram_head(node.kind, &node.name, &node.params, &node.result);
367    line.push(';');
368    w.line(&line);
369}
370
371/// `procedure P (…) is <declarations> begin <statements> end P;`
372fn render_subprogram_body(w: &mut Writer, node: &SubprogramBody) {
373    doc(w, &node.spec.doc);
374    let mut head = subprogram_head(
375        node.spec.kind,
376        &node.spec.name,
377        &node.spec.params,
378        &node.spec.result,
379    );
380    head.push_str(" is");
381    w.line(&head);
382    let split = node
383        .body
384        .iter()
385        .position(|statement| {
386            !matches!(
387                statement,
388                Stmt::Var(_) | Stmt::Subtype { .. } | Stmt::Comment(_)
389            )
390        })
391        .unwrap_or(node.body.len());
392    w.indent();
393    for statement in &node.body[..split] {
394        match statement {
395            Stmt::Var(var) => w.line(&var_text(var)),
396            Stmt::Subtype { name, mark } => w.line(&subtype_text(name, mark)),
397            Stmt::Comment(lines) => doc(w, lines),
398            _ => {}
399        }
400    }
401    w.dedent();
402    w.line("begin");
403    w.indent();
404    if node.body[split..].is_empty() {
405        w.line("null;");
406    } else {
407        render_stmts(w, &node.body[split..]);
408    }
409    w.dedent();
410    let mut tail = String::from("end ");
411    tail.push_str(node.spec.name.as_str());
412    tail.push(';');
413    w.line(&tail);
414}
415
416/// `Name : Mark (Constraint) := Value;`
417fn var_text(node: &VarDecl) -> String {
418    let mut out = String::from(node.name.as_str());
419    out.push_str(" : ");
420    out.push_str(&path_text(&node.mark));
421    if let Some(constraint) = &node.constraint {
422        out.push_str(" (");
423        out.push_str(&expr_text(constraint));
424        out.push(')');
425    }
426    if let Some(value) = &node.value {
427        out.push_str(" := ");
428        out.push_str(&expr_text(value));
429    }
430    out.push(';');
431    out
432}
433
434fn subtype_text(name: &Ident, mark: &Path) -> String {
435    let mut out = String::from("subtype ");
436    out.push_str(name.as_str());
437    out.push_str(" is ");
438    out.push_str(&path_text(mark));
439    out.push(';');
440    out
441}
442
443/// A statement list. A declaration met mid-list opens a `declare` block
444/// that scopes the rest of the list.
445///
446/// zerodds-lint: recursion-depth = the generated body's block nesting.
447/// The codec walk emits a routine per type rather than inlining one
448/// (`lang-rules/src/emit/encode.rs`), so nothing here follows the type
449/// graph. What does grow with the IDL is the dispatch chain: a union's
450/// cases, an enumeration's enumerators and a `@mutable` aggregate's
451/// members each become one `If` per arm, in the previous arm's `else`.
452/// The model bounds none of the three counts, and Ada 83 nests the arms in
453/// its own output too, so they cannot be walked iteratively the way
454/// `c.rs` walks them. No number is claimed: this signature returns no
455/// error, so nothing here could report one.
456fn render_stmts(w: &mut Writer, nodes: &[Stmt]) {
457    let mut at = 0;
458    while at < nodes.len() {
459        let statement = &nodes[at];
460        if matches!(statement, Stmt::Var(_) | Stmt::Subtype { .. }) {
461            w.line("declare");
462            w.indent();
463            while at < nodes.len() {
464                match &nodes[at] {
465                    Stmt::Var(var) => w.line(&var_text(var)),
466                    Stmt::Subtype { name, mark } => w.line(&subtype_text(name, mark)),
467                    Stmt::Comment(lines) => doc(w, lines),
468                    _ => break,
469                }
470                at += 1;
471            }
472            w.dedent();
473            w.line("begin");
474            w.indent();
475            if nodes[at..].is_empty() {
476                w.line("null;");
477            } else {
478                render_stmts(w, &nodes[at..]);
479            }
480            w.dedent();
481            w.line("end;");
482            return;
483        }
484        render_stmt(w, statement);
485        at += 1;
486    }
487}
488
489/// zerodds-lint: recursion-depth = the generated body's block nesting.
490/// The codec walk emits a routine per type rather than inlining one
491/// (`lang-rules/src/emit/encode.rs`), so nothing here follows the type
492/// graph. What does grow with the IDL is the dispatch chain: a union's
493/// cases, an enumeration's enumerators and a `@mutable` aggregate's
494/// members each become one `If` per arm, in the previous arm's `else`.
495/// The model bounds none of the three counts, and Ada 83 nests the arms in
496/// its own output too, so they cannot be walked iteratively the way
497/// `c.rs` walks them. No number is claimed: this signature returns no
498/// error, so nothing here could report one.
499fn render_stmt(w: &mut Writer, node: &Stmt) {
500    match node {
501        Stmt::Null => w.line("null;"),
502        Stmt::Var(var) => w.line(&var_text(var)),
503        Stmt::Subtype { name, mark } => w.line(&subtype_text(name, mark)),
504        Stmt::Assign { place, value } => {
505            let mut line = expr_text(place);
506            line.push_str(" := ");
507            line.push_str(&expr_text(value));
508            line.push(';');
509            w.line(&line);
510        }
511        Stmt::Call(value) => {
512            let mut line = expr_text(value);
513            line.push(';');
514            w.line(&line);
515        }
516        Stmt::Return(value) => match value {
517            Some(value) => {
518                let mut line = String::from("return ");
519                line.push_str(&expr_text(value));
520                line.push(';');
521                w.line(&line);
522            }
523            None => w.line("return;"),
524        },
525        Stmt::If {
526            condition,
527            then,
528            otherwise,
529        } => {
530            let mut head = String::from("if ");
531            head.push_str(&expr_text(condition));
532            head.push_str(" then");
533            w.line(&head);
534            w.indent();
535            if then.is_empty() {
536                w.line("null;");
537            } else {
538                render_stmts(w, then);
539            }
540            w.dedent();
541            if let Some(otherwise) = otherwise {
542                w.line("else");
543                w.indent();
544                if otherwise.is_empty() {
545                    w.line("null;");
546                } else {
547                    render_stmts(w, otherwise);
548                }
549                w.dedent();
550            }
551            w.line("end if;");
552        }
553        Stmt::For {
554            counter,
555            low,
556            high,
557            body,
558        } => {
559            let mut head = String::from("for ");
560            head.push_str(counter.as_str());
561            head.push_str(" in ");
562            head.push_str(&expr_text(low));
563            head.push_str(" .. ");
564            head.push_str(&expr_text(high));
565            head.push_str(" loop");
566            w.line(&head);
567            w.indent();
568            if body.is_empty() {
569                w.line("null;");
570            } else {
571                render_stmts(w, body);
572            }
573            w.dedent();
574            w.line("end loop;");
575        }
576        Stmt::While { condition, body } => {
577            let mut head = String::from("while ");
578            head.push_str(&expr_text(condition));
579            head.push_str(" loop");
580            w.line(&head);
581            w.indent();
582            if body.is_empty() {
583                w.line("null;");
584            } else {
585                render_stmts(w, body);
586            }
587            w.dedent();
588            w.line("end loop;");
589        }
590        Stmt::Exit => w.line("exit;"),
591        Stmt::Declare { vars, body } => {
592            w.line("declare");
593            w.indent();
594            for var in vars {
595                w.line(&var_text(var));
596            }
597            w.dedent();
598            w.line("begin");
599            w.indent();
600            if body.is_empty() {
601                w.line("null;");
602            } else {
603                render_stmts(w, body);
604            }
605            w.dedent();
606            w.line("end;");
607        }
608        Stmt::Comment(lines) => doc(w, lines),
609    }
610}
611
612fn param_text(node: &Param) -> String {
613    let mut out = String::from(node.name.as_str());
614    out.push_str(" : ");
615    out.push_str(mode_text(node.mode));
616    out.push(' ');
617    out.push_str(&path_text(&node.type_));
618    out
619}
620
621const fn mode_text(mode: Mode) -> &'static str {
622    mode.as_str()
623}
624
625/// zerodds-lint: recursion-depth = the built expression's nesting. A walk
626/// calls a routine per type rather than nesting one, so the one operand
627/// tree that grows with the IDL is an array's default value: one composite
628/// literal per dimension, built in `lang-rules/src/default_ada83.rs`, and
629/// nothing bounds the dimension count. No number is claimed: this
630/// signature returns no error.
631fn expr_text(node: &Expr) -> String {
632    match node {
633        Expr::Literal(literal) => literal_text(literal),
634        Expr::Name(name) => path_text(name),
635        Expr::Call { name, args } => {
636            let mut out = path_text(name);
637            // A parameterless call is the bare name (RM83 6.4).
638            if !args.is_empty() {
639                out.push_str(" (");
640                out.push_str(
641                    &args
642                        .iter()
643                        .map(expr_text)
644                        .collect::<Vec<String>>()
645                        .join(", "),
646                );
647                out.push(')');
648            }
649            out
650        }
651        Expr::Negate(inner) => {
652            let mut out = String::from("-");
653            out.push_str(&expr_text(inner));
654            out
655        }
656        Expr::Binary { op, lhs, rhs } => {
657            let mut out = operand_text(lhs);
658            out.push(' ');
659            out.push_str(ada_op(op));
660            out.push(' ');
661            out.push_str(&operand_text(rhs));
662            out
663        }
664        Expr::Unary { op, value } => {
665            let mut out = String::from(ada_op(op));
666            if *op != "-" {
667                out.push(' ');
668            }
669            out.push_str(&operand_text(value));
670            out
671        }
672        Expr::Selected { value, name } => {
673            let mut out = operand_text(value);
674            out.push('.');
675            out.push_str(name.as_str());
676            out
677        }
678        Expr::Indexed { value, index } => {
679            let mut out = operand_text(value);
680            out.push_str(" (");
681            out.push_str(&expr_text(index));
682            out.push(')');
683            out
684        }
685        Expr::Deref(value) => {
686            let mut out = operand_text(value);
687            out.push_str(".all");
688            out
689        }
690        Expr::Slice { value, low, high } => {
691            let mut out = operand_text(value);
692            out.push_str(" (");
693            out.push_str(&expr_text(low));
694            out.push_str(" .. ");
695            out.push_str(&expr_text(high));
696            out.push(')');
697            out
698        }
699        Expr::Attribute { value, name, args } => {
700            let mut out = operand_text(value);
701            out.push('\'');
702            out.push_str(name.as_str());
703            if !args.is_empty() {
704                out.push_str(" (");
705                out.push_str(
706                    &args
707                        .iter()
708                        .map(expr_text)
709                        .collect::<Vec<String>>()
710                        .join(", "),
711                );
712                out.push(')');
713            }
714            out
715        }
716        Expr::Convert { mark, value } => {
717            let mut out = path_text(mark);
718            out.push_str(" (");
719            out.push_str(&expr_text(value));
720            out.push(')');
721            out
722        }
723        Expr::Aggregate(parts) => {
724            let mut out = String::from("(");
725            out.push_str(
726                &parts
727                    .iter()
728                    .map(|part| {
729                        let mut one = String::from(part.choice.as_str());
730                        one.push_str(" => ");
731                        one.push_str(&expr_text(&part.value));
732                        one
733                    })
734                    .collect::<Vec<String>>()
735                    .join(", "),
736            );
737            out.push(')');
738            out
739        }
740        Expr::New { mark, init } => {
741            let mut out = String::from("new ");
742            out.push_str(&path_text(mark));
743            match init {
744                AllocInit::Range { low, high } => {
745                    out.push_str(" (");
746                    out.push_str(&expr_text(low));
747                    out.push_str(" .. ");
748                    out.push_str(&expr_text(high));
749                    out.push(')');
750                }
751                AllocInit::Qualified(value) => {
752                    out.push_str("'(");
753                    out.push_str(&expr_text(value));
754                    out.push(')');
755                }
756            }
757            out
758        }
759    }
760}
761
762/// An operand, parenthesised where its own operator could re-associate.
763///
764/// zerodds-lint: recursion-depth = the built expression's nesting. A walk
765/// calls a routine per type rather than nesting one, so the one operand
766/// tree that grows with the IDL is an array's default value: one composite
767/// literal per dimension, built in `lang-rules/src/default_ada83.rs`, and
768/// nothing bounds the dimension count. No number is claimed: this
769/// signature returns no error.
770fn operand_text(node: &Expr) -> String {
771    match node {
772        Expr::Binary { .. } | Expr::Unary { .. } | Expr::Negate(_) => {
773            let mut out = String::from("(");
774            out.push_str(&expr_text(node));
775            out.push(')');
776            out
777        }
778        _ => expr_text(node),
779    }
780}
781
782/// The Ada spelling of the shared walk's operator.
783fn ada_op(op: &str) -> &str {
784    match op {
785        "==" => "=",
786        "!=" => "/=",
787        "&&" => "and then",
788        "||" => "or else",
789        "&" => "and",
790        "|" => "or",
791        "^" => "xor",
792        "%" => "mod",
793        "!" | "~" => "not",
794        other => other,
795    }
796}
797
798fn literal_text(literal: &Literal) -> String {
799    match literal {
800        Literal::Int(text) | Literal::Float(text) => text.clone(),
801        Literal::Bool(value) => {
802            if *value {
803                "True".to_string()
804            } else {
805                "False".to_string()
806            }
807        }
808        // The code point as a number: an IDL `char`/`wchar` is a
809        // `Long_Integer` in this column, not `Standard.Character`.
810        Literal::Char(ch) => alloc::format!("{}", u32::from(*ch)),
811        Literal::Str(text) => ada_string(text),
812        Literal::Bytes(bytes) => {
813            let mut out = String::from("(");
814            let parts: Vec<String> = bytes
815                .iter()
816                .map(|b| alloc::format!("16#{b:02X}#"))
817                .collect();
818            out.push_str(&parts.join(", "));
819            out.push(')');
820            out
821        }
822        Literal::Null => "null".to_string(),
823    }
824}
825
826/// An Ada string literal — RM83 2.6 doubles the quote, unchanged in
827/// every later revision, and there is no backslash escape.
828fn ada_string(text: &str) -> String {
829    let mut out = String::from("\"");
830    for ch in text.chars() {
831        if ch == '"' {
832            out.push('"');
833        }
834        out.push(ch);
835    }
836    out.push('"');
837    out
838}
839
840fn doc(w: &mut Writer, doc: &Doc) {
841    if doc.is_empty() {
842        return;
843    }
844    comment_lines(w, "--  ", doc.as_lines());
845}
846
847#[cfg(test)]
848mod tests {
849    #![allow(clippy::expect_used, clippy::panic, clippy::unwrap_used)]
850    use super::*;
851    use alloc::vec;
852    use zerodds_lang_cst::ada83::{Primitive, Record};
853
854    fn component(name: &str, type_: Primitive) -> Component {
855        Component {
856            doc: Doc::none(),
857            name: Ident::new(name),
858            type_: type_.path(),
859        }
860    }
861
862    #[test]
863    fn a_record_writes_one_component_per_line() {
864        let node = Decl::Type(TypeDecl {
865            doc: Doc::none(),
866            name: Ident::new("Pose"),
867            def: TypeDef::Record(Record::plain(vec![
868                component("X", Primitive::LongInteger),
869                component("Y", Primitive::LongFloat),
870            ])),
871        });
872        assert_eq!(
873            decl(&node),
874            "type Pose is record\n   \
875             X : Long_Integer;\n   \
876             Y : Long_Float;\n\
877             end record;\n"
878        );
879    }
880
881    #[test]
882    fn a_record_with_no_components_is_a_null_record() {
883        let node = Decl::Type(TypeDecl {
884            doc: Doc::none(),
885            name: Ident::new("Empty"),
886            def: TypeDef::Record(Record::plain(Vec::new())),
887        });
888        assert_eq!(decl(&node), "type Empty is null record;\n");
889    }
890
891    #[test]
892    fn a_discriminated_record_writes_its_variant_part() {
893        let node = Decl::Type(TypeDecl {
894            doc: Doc::none(),
895            name: Ident::new("Payload"),
896            def: TypeDef::Record(Record {
897                body_text: zerodds_lang_cst::common::BodyText::none(),
898                discriminants: vec![Discriminant {
899                    name: Ident::new("D"),
900                    type_: Primitive::LongInteger.path(),
901                    default: Some(Expr::int(0)),
902                }],
903                components: Vec::new(),
904                variant: Some(Variant {
905                    discriminant: Ident::new("D"),
906                    arms: vec![
907                        Arm {
908                            choices: vec![Choice::Value(Expr::int(0))],
909                            components: vec![component("Idle", Primitive::LongInteger)],
910                        },
911                        Arm {
912                            choices: vec![Choice::Others],
913                            components: Vec::new(),
914                        },
915                    ],
916                }),
917            }),
918        });
919        assert_eq!(
920            decl(&node),
921            "type Payload (D : Long_Integer := 0) is record\n   \
922             case D is\n      \
923             when 0 =>\n         \
924             Idle : Long_Integer;\n      \
925             when others =>\n         \
926             null;\n   \
927             end case;\n\
928             end record;\n"
929        );
930    }
931
932    #[test]
933    fn no_enumeration_representation_clause_form_exists() {
934        // Confirms the CST cannot even express what the Ada 2012 column
935        // writes: an enumerator here is an object declaration, not a
936        // type plus a clause. See the module documentation.
937        let node = Decl::Object(ObjectDecl {
938            doc: Doc::none(),
939            name: Ident::new("ACTIVE"),
940            type_: Primitive::LongInteger.path(),
941            value: Expr::int(4),
942        });
943        assert_eq!(decl(&node), "ACTIVE : constant Long_Integer := 4;\n");
944    }
945
946    #[test]
947    fn an_unconstrained_array_and_the_access_type_that_reaches_it() {
948        let array = Decl::Type(TypeDecl {
949            doc: Doc::none(),
950            name: Ident::new("Zd_Seq_Long_Array"),
951            def: TypeDef::Array {
952                index: vec![Index::Unconstrained(Path::single("Natural"))],
953                element: Primitive::LongInteger.path(),
954            },
955        });
956        let access = Decl::Type(TypeDecl {
957            doc: Doc::none(),
958            name: Ident::new("Zd_Seq_Long"),
959            def: TypeDef::Access(Path::single("Zd_Seq_Long_Array")),
960        });
961        assert_eq!(
962            decl(&array),
963            "type Zd_Seq_Long_Array is array (Natural range <>) of Long_Integer;\n"
964        );
965        assert_eq!(
966            decl(&access),
967            "type Zd_Seq_Long is access Zd_Seq_Long_Array;\n"
968        );
969    }
970
971    #[test]
972    fn a_string_is_an_access_to_character_not_unbounded_string() {
973        let array = Decl::Type(TypeDecl {
974            doc: Doc::none(),
975            name: Ident::new("Zd_Str_Array"),
976            def: TypeDef::Array {
977                index: vec![Index::Unconstrained(Path::single("Natural"))],
978                element: Path::single("Character"),
979            },
980        });
981        assert_eq!(
982            decl(&array),
983            "type Zd_Str_Array is array (Natural range <>) of Character;\n"
984        );
985    }
986
987    #[test]
988    fn an_incomplete_type_declares_a_name_and_nothing_else() {
989        let incomplete = Decl::Type(TypeDecl {
990            doc: Doc::none(),
991            name: Ident::new("Node"),
992            def: TypeDef::Incomplete,
993        });
994        assert_eq!(decl(&incomplete), "type Node;\n");
995    }
996
997    #[test]
998    fn a_subprogram_has_no_is_abstract_form() {
999        // Unlike the Ada 2012 tree, `Subprogram` has no such field: this
1000        // is always a forward declaration for a recursive pair.
1001        let node = Decl::Subprogram(Subprogram {
1002            doc: Doc::none(),
1003            kind: SubprogramKind::Function,
1004            name: Ident::new("Zd_Dec_Node"),
1005            params: vec![Param {
1006                name: Ident::new("R"),
1007                mode: Mode::InOut,
1008                type_: Path::new(&["Zd_Wire83", "Reader"]),
1009            }],
1010            result: Some(Path::single("Node")),
1011        });
1012        assert_eq!(
1013            decl(&node),
1014            "function Zd_Dec_Node (R : in out Zd_Wire83.Reader) return Node;\n"
1015        );
1016    }
1017
1018    #[test]
1019    fn a_file_writes_its_context_clause_before_the_package() {
1020        let node = File {
1021            doc: Doc::line("Generated. Do not edit."),
1022            withs: vec![Path::single("Zd_Wire83")],
1023            package: Ident::new("Zd83_Types"),
1024            decls: vec![Decl::Type(TypeDecl {
1025                doc: Doc::none(),
1026                name: Ident::new("Pose"),
1027                def: TypeDef::Record(Record::plain(vec![component("X", Primitive::LongInteger)])),
1028            })],
1029        };
1030        assert_eq!(
1031            file(&node),
1032            "--  Generated. Do not edit.\n\
1033             \n\
1034             with Zd_Wire83;\n\
1035             \n\
1036             package Zd83_Types is\n\
1037             \n   \
1038             type Pose is record\n      \
1039             X : Long_Integer;\n   \
1040             end record;\n\
1041             \n\
1042             end Zd83_Types;\n"
1043        );
1044    }
1045
1046    #[test]
1047    fn a_boolean_is_written_with_the_case_ada_declares_it_in() {
1048        assert_eq!(expr(&Expr::Literal(Literal::Bool(true))), "True");
1049        assert_eq!(expr(&Expr::Literal(Literal::Bool(false))), "False");
1050    }
1051
1052    #[test]
1053    fn a_hexadecimal_literal_uses_the_ada_base_form() {
1054        assert_eq!(
1055            expr(&Expr::Literal(Literal::Bytes(alloc::vec![1, 255]))),
1056            "(16#01#, 16#FF#)"
1057        );
1058    }
1059
1060    #[test]
1061    fn every_aggregate_carries_at_least_one_association() {
1062        // No `BoxDefault`, no way to write an empty aggregate: this
1063        // tree cannot spell `(null record)` at all. See the module
1064        // documentation.
1065        let node = Expr::Aggregate(vec![zerodds_lang_cst::ada83::Association {
1066            choice: Ident::new("D"),
1067            value: Expr::int(0),
1068        }]);
1069        assert_eq!(expr(&node), "(D => 0)");
1070    }
1071
1072    #[test]
1073    fn selection_indexing_dereference_and_slice_render_as_ada_writes_them() {
1074        let name = |text: &str| Expr::name(text);
1075        let selected = Expr::Selected {
1076            value: alloc::boxed::Box::new(name("v")),
1077            name: Ident::new("Key"),
1078        };
1079        assert_eq!(expr(&selected), "v.Key");
1080        let deref = Expr::Deref(alloc::boxed::Box::new(name("v")));
1081        assert_eq!(expr(&deref), "v.all");
1082        let slice = Expr::Slice {
1083            value: alloc::boxed::Box::new(deref),
1084            low: alloc::boxed::Box::new(Expr::int(0)),
1085            high: alloc::boxed::Box::new(name("zd_n")),
1086        };
1087        assert_eq!(expr(&slice), "v.all (0 .. zd_n)");
1088    }
1089
1090    #[test]
1091    fn an_allocator_takes_a_range_or_a_qualified_value() {
1092        let empty = Expr::New {
1093            mark: Path::single("Zd_Seq_Long_Array"),
1094            init: AllocInit::Range {
1095                low: alloc::boxed::Box::new(Expr::int(0)),
1096                high: alloc::boxed::Box::new(Expr::int(-1)),
1097            },
1098        };
1099        assert_eq!(expr(&empty), "new Zd_Seq_Long_Array (0 .. -1)");
1100        let kept = Expr::New {
1101            mark: Path::single("Zd_Seq_Long_Array"),
1102            init: AllocInit::Qualified(alloc::boxed::Box::new(Expr::name("v"))),
1103        };
1104        assert_eq!(expr(&kept), "new Zd_Seq_Long_Array'(v)");
1105    }
1106
1107    #[test]
1108    fn a_declaration_mid_list_opens_a_declare_block_over_the_rest() {
1109        let nodes = vec![
1110            Stmt::Assign {
1111                place: Expr::name("a"),
1112                value: Expr::int(1),
1113            },
1114            Stmt::Var(VarDecl {
1115                name: Ident::new("zd_n"),
1116                mark: Primitive::LongInteger.path(),
1117                constraint: None,
1118                value: Some(Expr::int(0)),
1119            }),
1120            Stmt::Assign {
1121                place: Expr::name("a"),
1122                value: Expr::name("zd_n"),
1123            },
1124        ];
1125        assert_eq!(
1126            stmts(&nodes),
1127            "a := 1;\n\
1128             declare\n   \
1129             zd_n : Long_Integer := 0;\n\
1130             begin\n   \
1131             a := zd_n;\n\
1132             end;\n"
1133        );
1134    }
1135
1136    #[test]
1137    fn the_loop_forms_and_exit_render() {
1138        let counted = Stmt::For {
1139            counter: Ident::new("zd_i"),
1140            low: Expr::int(0),
1141            high: Expr::int(2),
1142            body: vec![Stmt::Null],
1143        };
1144        assert_eq!(
1145            stmt(&counted),
1146            "for zd_i in 0 .. 2 loop\n   null;\nend loop;\n"
1147        );
1148        let headed = Stmt::While {
1149            condition: Expr::Literal(Literal::Bool(true)),
1150            body: vec![Stmt::Exit],
1151        };
1152        assert_eq!(stmt(&headed), "while True loop\n   exit;\nend loop;\n");
1153    }
1154
1155    #[test]
1156    fn an_if_writes_its_else_and_an_empty_arm_is_null() {
1157        let node = Stmt::If {
1158            condition: Expr::name("ok"),
1159            then: Vec::new(),
1160            otherwise: Some(vec![Stmt::Return(None)]),
1161        };
1162        assert_eq!(
1163            stmt(&node),
1164            "if ok then\n   null;\nelse\n   return;\nend if;\n"
1165        );
1166    }
1167}