Skip to main content

zerodds_lang_render/
go.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2026 ZeroDDS Contributors
3//! Rendering the Go CST. Spec §5.
4//!
5//! One style, `gofmt`'s: tabs for indentation, the opening brace on the
6//! same line, `//` comments. Tabs and not spaces because `gofmt` is not
7//! a preference in Go — `go vet` does not check formatting, but every
8//! reader of a generated file runs it, and a file that changes under it
9//! is a diff nobody asked for.
10//!
11//! Nothing is decided here. A Go type reads left to right and carries
12//! its own length and pointer depth, so unlike
13//! [`crate::c`] there is no declarator to split around a name:
14//! a field is the name, a space, and the type.
15
16use crate::writer::{Writer, comment_lines, escape_string, verbatim_lines};
17use alloc::string::{String, ToString};
18use alloc::vec::Vec;
19use zerodds_lang_cst::common::{Doc, Ident, Literal};
20use zerodds_lang_cst::go::{
21    ConstDecl, Decl, Expr, Field, File, FuncDecl, Import, InterfaceDecl, Method, Param, Stmt, Tag,
22    Type, TypeDecl,
23};
24
25/// Render a whole file.
26#[must_use]
27pub fn file(file: &File) -> String {
28    let mut w = Writer::new("\t");
29    doc(&mut w, &file.doc);
30    let mut package = String::from("package ");
31    package.push_str(file.package.as_str());
32    w.line(&package);
33    if !file.imports.is_empty() {
34        w.blank_line();
35        render_imports(&mut w, &file.imports);
36    }
37    for decl in &file.decls {
38        w.blank_line();
39        render_decl(&mut w, decl);
40    }
41    w.finish()
42}
43
44/// Render one declaration, for a caller that has only one.
45#[must_use]
46pub fn decl(node: &Decl) -> String {
47    let mut w = Writer::new("\t");
48    render_decl(&mut w, node);
49    w.finish()
50}
51
52/// Render one type, for a caller that has only one.
53#[must_use]
54pub fn type_(node: &Type) -> String {
55    type_text(node)
56}
57
58/// A single import is written on one line; two or more take the
59/// parenthesised group `gofmt` produces.
60fn render_imports(w: &mut Writer, imports: &[Import]) {
61    if let [only] = imports {
62        let mut line = String::from("import ");
63        line.push_str(&import_text(only));
64        w.line(&line);
65        return;
66    }
67    w.line("import (");
68    w.indent();
69    for import in imports {
70        w.line(&import_text(import));
71    }
72    w.dedent();
73    w.line(")");
74}
75
76fn import_text(import: &Import) -> String {
77    let mut out = String::new();
78    if let Some(alias) = &import.alias {
79        out.push_str(alias.as_str());
80        out.push(' ');
81    }
82    out.push('"');
83    out.push_str(&escape_string(&import.path));
84    out.push('"');
85    out
86}
87
88fn render_decl(w: &mut Writer, node: &Decl) {
89    match node {
90        Decl::Type(decl) => render_type_decl(w, decl),
91        Decl::Const(decl) => render_const(w, decl),
92        Decl::Interface(decl) => render_interface(w, decl),
93        Decl::Func(decl) => render_func(w, decl),
94        Decl::Comment(lines) => doc(w, lines),
95        Decl::Verbatim(text) => verbatim_lines(w, text),
96    }
97}
98
99/// `func name(params) results { body }`.
100fn render_func(w: &mut Writer, node: &FuncDecl) {
101    doc(w, &node.doc);
102    let mut head = String::from("func ");
103    head.push_str(node.name.as_str());
104    head.push('(');
105    let params: Vec<String> = node.params.iter().map(param_text).collect();
106    head.push_str(&params.join(", "));
107    head.push(')');
108    match node.results.as_slice() {
109        [] => {}
110        [only] => {
111            head.push(' ');
112            head.push_str(&type_text(only));
113        }
114        many => {
115            head.push_str(" (");
116            let parts: Vec<String> = many.iter().map(type_text).collect();
117            head.push_str(&parts.join(", "));
118            head.push(')');
119        }
120    }
121    head.push_str(" {");
122    w.line(&head);
123    w.indent();
124    render_stmts(w, &node.body);
125    w.dedent();
126    w.line("}");
127}
128
129/// zerodds-lint: recursion-depth 64 (one level per nested block of a
130/// generated body. The codec walk emits a routine per type rather than
131/// inlining one — `lang-rules/src/emit/encode.rs` — so a body nests by
132/// its own framing and by one loop per array dimension, never by the
133/// type graph, whose descent stops at 64 anyway; the else-if chain, one
134/// arm per union case, is walked iteratively and costs no depth.)
135fn render_stmts(w: &mut Writer, body: &[Stmt]) {
136    for statement in body {
137        render_stmt(w, statement);
138    }
139}
140
141/// zerodds-lint: recursion-depth 64 (one level per nested block of a
142/// generated body. The codec walk emits a routine per type rather than
143/// inlining one — `lang-rules/src/emit/encode.rs` — so a body nests by
144/// its own framing and by one loop per array dimension, never by the
145/// type graph, whose descent stops at 64 anyway; the else-if chain, one
146/// arm per union case, is walked iteratively and costs no depth.)
147fn render_stmt(w: &mut Writer, node: &Stmt) {
148    match node {
149        Stmt::Var { name, type_, value } => {
150            let mut line;
151            match (type_, value) {
152                // `name := value`: the type is the initialiser's.
153                (None, Some(value)) => {
154                    line = String::from(name.as_str());
155                    line.push_str(" := ");
156                    line.push_str(&expr_text(value));
157                }
158                (Some(type_), value) => {
159                    line = String::from("var ");
160                    line.push_str(name.as_str());
161                    line.push(' ');
162                    line.push_str(&type_text(type_));
163                    if let Some(value) = value {
164                        line.push_str(" = ");
165                        line.push_str(&expr_text(value));
166                    }
167                }
168                // A declaration with neither type nor value is not Go;
169                // written out so the defect names itself in the diff
170                // rather than compiling into something else.
171                (None, None) => {
172                    line = String::from("var ");
173                    line.push_str(name.as_str());
174                }
175            }
176            w.line(&line);
177        }
178        Stmt::Assign { place, value } => {
179            let mut line = expr_text(place);
180            line.push_str(" = ");
181            line.push_str(&expr_text(value));
182            w.line(&line);
183        }
184        Stmt::Expr(value) => w.line(&expr_text(value)),
185        Stmt::Return(value) => match value {
186            Some(value) => {
187                let mut line = String::from("return ");
188                line.push_str(&expr_text(value));
189                w.line(&line);
190            }
191            None => w.line("return"),
192        },
193        Stmt::If {
194            condition,
195            then,
196            otherwise,
197        } => render_if(w, condition, then, otherwise.as_deref(), "if "),
198        Stmt::For {
199            counter,
200            init,
201            bound,
202            body,
203        } => {
204            let mut line = String::from("for ");
205            line.push_str(counter.as_str());
206            line.push_str(" := ");
207            line.push_str(&expr_text(init));
208            line.push_str("; ");
209            line.push_str(counter.as_str());
210            line.push_str(" < ");
211            line.push_str(&expr_text(bound));
212            line.push_str("; ");
213            line.push_str(counter.as_str());
214            line.push_str("++ {");
215            w.line(&line);
216            w.indent();
217            render_stmts(w, body);
218            w.dedent();
219            w.line("}");
220        }
221        Stmt::While { condition, body } => {
222            let mut line = String::from("for ");
223            line.push_str(&expr_text(condition));
224            line.push_str(" {");
225            w.line(&line);
226            w.indent();
227            render_stmts(w, body);
228            w.dedent();
229            w.line("}");
230        }
231        Stmt::Break => w.line("break"),
232        Stmt::Comment(lines) => doc(w, lines),
233    }
234}
235
236/// An `if` chain, with `else if` written the way `gofmt` writes it: an
237/// else-branch holding exactly one `if` and nothing else continues the
238/// line rather than nesting.
239///
240/// The next arm is taken in a loop and not by a call to this function
241/// again. The chain's length is the union's case count, the
242/// enumeration's enumerator count or the `@mutable` aggregate's member
243/// count — `lang-rules/src/emit/encode.rs` builds one `If` per arm, each
244/// in the previous one's `else` — and nothing in the model bounds any of
245/// the three.
246///
247/// zerodds-lint: recursion-depth 64 (block nesting only; the arm chain
248/// is walked iteratively, and what remains is one level per nested block
249/// of a generated body — the codec walk emits a routine per type rather
250/// than inlining, so a body nests by its framing and by one loop per
251/// array dimension, never by the type graph)
252fn render_if(
253    w: &mut Writer,
254    condition: &Expr,
255    then: &[Stmt],
256    otherwise: Option<&[Stmt]>,
257    head: &str,
258) {
259    let mut condition = condition;
260    let mut then = then;
261    let mut otherwise = otherwise;
262    let mut head = head;
263    loop {
264        let mut line = String::from(head);
265        line.push_str(&expr_text(condition));
266        line.push_str(" {");
267        w.line(&line);
268        w.indent();
269        render_stmts(w, then);
270        w.dedent();
271        match otherwise {
272            None => {
273                w.line("}");
274                return;
275            }
276            Some(
277                [
278                    Stmt::If {
279                        condition: next_condition,
280                        then: next_then,
281                        otherwise: next_otherwise,
282                    },
283                ],
284            ) => {
285                condition = next_condition;
286                then = next_then;
287                otherwise = next_otherwise.as_deref();
288                head = "} else if ";
289            }
290            Some(branch) => {
291                w.line("} else {");
292                w.indent();
293                render_stmts(w, branch);
294                w.dedent();
295                w.line("}");
296                return;
297            }
298        }
299    }
300}
301
302fn render_type_decl(w: &mut Writer, node: &TypeDecl) {
303    doc(w, &node.doc);
304    let mut head = String::from("type ");
305    head.push_str(node.name.as_str());
306    head.push(' ');
307    if node.alias {
308        head.push_str("= ");
309    }
310    match &node.type_ {
311        Type::Struct(fields) => {
312            head.push_str("struct {");
313            w.line(&head);
314            w.indent();
315            for text in &node.body_text.begin {
316                verbatim_lines(w, text);
317            }
318            render_fields(w, fields);
319            for text in &node.body_text.end {
320                verbatim_lines(w, text);
321            }
322            w.dedent();
323            w.line("}");
324        }
325        other => {
326            head.push_str(&type_text(other));
327            w.line(&head);
328        }
329    }
330}
331
332/// A struct's fields, with the type and tag columns aligned.
333///
334/// `gofmt` aligns the cells of consecutive field lines, and a generated
335/// file that changes under `gofmt` is a diff nobody asked for, on a file
336/// nobody edited. So the renderer aligns them itself, and
337/// `the_generated_go_is_gofmt_stable` in `go_end_to_end.rs` is what says
338/// it got the rule right rather than plausible.
339///
340/// The rule, measured against `gofmt` over the whole corpus: pad the
341/// name to the widest name and the type to the widest type **within a
342/// run**, with the single space `gofmt` leaves between columns. A field
343/// carrying a documentation comment **starts a new run** — `go/printer`
344/// writes through a `text/tabwriter`, and a line with fewer cells than
345/// the block (a comment line has one) terminates the column block. Taking
346/// the widths over the whole field list instead left fourteen of the
347/// corpus's four hundred and eleven files changing under `gofmt`, all of
348/// them structs with an `@optional` member, which is the member this
349/// column documents.
350///
351/// What is deliberately not implemented is `go/printer`'s outlier rule,
352/// which starts a new section when one cell is far wider than the
353/// running mean. Nothing this column emits reaches it: a field's name is
354/// an IDL identifier and its type is a Go type spelled inline.
355fn render_fields(w: &mut Writer, fields: &[Field]) {
356    let mut run: Vec<&Field> = Vec::new();
357    for field in fields {
358        if !field.doc.is_empty() && !run.is_empty() {
359            flush_run(w, &run);
360            run.clear();
361        }
362        run.push(field);
363    }
364    flush_run(w, &run);
365}
366
367/// One alignment block: the widths are this run's and no wider.
368fn flush_run(w: &mut Writer, run: &[&Field]) {
369    let name_width = run
370        .iter()
371        .map(|field| field.name.as_str().chars().count())
372        .max()
373        .unwrap_or(0);
374    let type_width = run
375        .iter()
376        .filter(|field| field.tag.is_some())
377        .map(|field| type_text(&field.type_).chars().count())
378        .max()
379        .unwrap_or(0);
380    for field in run {
381        render_field(w, field, name_width, type_width);
382    }
383}
384
385fn render_field(w: &mut Writer, node: &Field, name_width: usize, type_width: usize) {
386    doc(w, &node.doc);
387    let mut line = String::from(node.name.as_str());
388    pad_to(&mut line, name_width);
389    line.push(' ');
390    let type_ = type_text(&node.type_);
391    line.push_str(&type_);
392    if let Some(tag) = &node.tag {
393        pad_to(&mut line, name_width + 1 + type_width);
394        line.push(' ');
395        line.push_str(&tag_text(tag));
396    }
397    w.line(&line);
398}
399
400/// Pad with spaces until the line is `width` characters wide.
401///
402/// Characters and not bytes: an identifier the IDL wrote is ASCII
403/// (IDL 4.2 §7.2.3), but a type name reached through a facet need not
404/// be, and `gofmt` counts what it prints.
405fn pad_to(line: &mut String, width: usize) {
406    while line.chars().count() < width {
407        line.push(' ');
408    }
409}
410
411/// A struct tag, in the raw-string form `gofmt` and `reflect` expect.
412///
413/// Back quotes rather than a double-quoted literal: a tag is read back
414/// with `reflect.StructTag.Get`, whose documented convention is
415/// `key:"value"` pairs, and writing that inside a double-quoted string
416/// means escaping every quote. The raw form cannot carry a back quote,
417/// and nothing put here can hold one — an IDL identifier is
418/// `[A-Za-z_][A-Za-z0-9_]*` (IDL 4.2 §7.2.3) and the keys are this
419/// column's own.
420fn tag_text(tag: &Tag) -> String {
421    let mut out = String::from("`");
422    for (index, (key, value)) in tag.entries.iter().enumerate() {
423        if index > 0 {
424            out.push(' ');
425        }
426        out.push_str(key);
427        out.push_str(":\"");
428        out.push_str(value);
429        out.push('"');
430    }
431    out.push('`');
432    out
433}
434
435fn render_const(w: &mut Writer, node: &ConstDecl) {
436    doc(w, &node.doc);
437    let mut line = String::from("const ");
438    line.push_str(node.name.as_str());
439    if let Some(type_) = &node.type_ {
440        line.push(' ');
441        line.push_str(&type_text(type_));
442    }
443    line.push_str(" = ");
444    line.push_str(&expr_text(&node.value));
445    w.line(&line);
446}
447
448fn render_interface(w: &mut Writer, node: &InterfaceDecl) {
449    doc(w, &node.doc);
450    let mut head = String::from("type ");
451    head.push_str(node.name.as_str());
452    head.push_str(" interface {");
453    w.line(&head);
454    w.indent();
455    for method in &node.methods {
456        render_method(w, method);
457    }
458    w.dedent();
459    w.line("}");
460}
461
462fn render_method(w: &mut Writer, node: &Method) {
463    doc(w, &node.doc);
464    let mut line = String::from(node.name.as_str());
465    line.push('(');
466    let params: Vec<String> = node.params.iter().map(param_text).collect();
467    line.push_str(&params.join(", "));
468    line.push(')');
469    match node.results.as_slice() {
470        [] => {}
471        [only] => {
472            line.push(' ');
473            line.push_str(&type_text(only));
474        }
475        many => {
476            line.push_str(" (");
477            let parts: Vec<String> = many.iter().map(type_text).collect();
478            line.push_str(&parts.join(", "));
479            line.push(')');
480        }
481    }
482    w.line(&line);
483}
484
485fn param_text(param: &Param) -> String {
486    let mut out = String::new();
487    if let Some(name) = &param.name {
488        out.push_str(name.as_str());
489        out.push(' ');
490    }
491    out.push_str(&type_text(&param.type_));
492    out
493}
494
495/// A struct type written inline, for a field whose type is a struct.
496///
497/// Reached only through [`type_text`]; a top-level `type T struct` is
498/// written by [`render_type_decl`], which can indent it against the
499/// declaration.
500fn struct_text(fields: &[Field]) -> String {
501    if fields.is_empty() {
502        return "struct{}".to_string();
503    }
504    let mut w = Writer::new("\t");
505    w.line("struct {");
506    w.indent();
507    render_fields(&mut w, fields);
508    w.dedent();
509    w.push("}");
510    w.finish().trim_end().to_string()
511}
512
513/// zerodds-lint: recursion-depth 64 (one level per collection, alias or
514/// generic argument of the spelled type; `MAX_TYPE_DEPTH` in
515/// `lang-rules/src/emit/go.rs` refuses a type that nests deeper)
516fn type_text(node: &Type) -> String {
517    match node {
518        Type::Primitive(primitive) => primitive.as_str().to_string(),
519        Type::Named(name) => name.as_str().to_string(),
520        Type::Slice(element) => {
521            let mut out = String::from("[]");
522            out.push_str(&type_text(element));
523            out
524        }
525        Type::Array { length, element } => {
526            let mut out = String::from("[");
527            out.push_str(&expr_text(length));
528            out.push(']');
529            out.push_str(&type_text(element));
530            out
531        }
532        Type::Pointer(target) => {
533            let mut out = String::from("*");
534            out.push_str(&type_text(target));
535            out
536        }
537        Type::Struct(fields) => struct_text(fields),
538    }
539}
540
541/// zerodds-lint: recursion-depth = the built expression's nesting. A walk
542/// calls a routine per type rather than nesting one, so the one operand
543/// tree that grows with the IDL is an array's default value: one composite
544/// literal per dimension, built in `lang-rules/src/default_go.rs`, and
545/// nothing bounds the dimension count. No number is claimed: this
546/// signature returns no error.
547fn expr_text(node: &Expr) -> String {
548    match node {
549        Expr::Literal(literal) => literal_text(literal),
550        Expr::Name(name) => name.as_str().to_string(),
551        Expr::Binary { op, lhs, rhs } => {
552            let mut out = expr_text(lhs);
553            out.push(' ');
554            out.push_str(op);
555            out.push(' ');
556            out.push_str(&expr_text(rhs));
557            out
558        }
559        Expr::Convert { type_, value } => {
560            let mut out = type_text(type_);
561            out.push('(');
562            out.push_str(&expr_text(value));
563            out.push(')');
564            out
565        }
566        Expr::Paren(inner) => {
567            let mut out = String::from("(");
568            out.push_str(&expr_text(inner));
569            out.push(')');
570            out
571        }
572        Expr::Call { func, args } => {
573            let mut out = expr_text(func);
574            out.push('(');
575            let parts: Vec<String> = args.iter().map(expr_text).collect();
576            out.push_str(&parts.join(", "));
577            out.push(')');
578            out
579        }
580        Expr::Selector { value, name } => {
581            let mut out = expr_text(value);
582            out.push('.');
583            out.push_str(name.as_str());
584            out
585        }
586        Expr::Index { value, index } => {
587            let mut out = expr_text(value);
588            out.push('[');
589            out.push_str(&expr_text(index));
590            out.push(']');
591            out
592        }
593        Expr::SliceTo { value, high } => {
594            let mut out = expr_text(value);
595            out.push_str("[:");
596            out.push_str(&expr_text(high));
597            out.push(']');
598            out
599        }
600        Expr::Unary { op, value } => {
601            let mut out = String::from(*op);
602            out.push_str(&expr_text(value));
603            out
604        }
605        Expr::Composite { type_, elems } => {
606            let mut out = match type_ {
607                Some(type_) => type_text(type_),
608                None => String::new(),
609            };
610            out.push('{');
611            let parts: Vec<String> = elems
612                .iter()
613                .map(|elem| match &elem.key {
614                    Some(key) => {
615                        let mut part = String::from(key.as_str());
616                        part.push_str(": ");
617                        part.push_str(&expr_text(&elem.value));
618                        part
619                    }
620                    None => expr_text(&elem.value),
621                })
622                .collect();
623            out.push_str(&parts.join(", "));
624            out.push('}');
625            out
626        }
627        Expr::Make { type_, length } => {
628            let mut out = String::from("make(");
629            out.push_str(&type_text(type_));
630            out.push_str(", ");
631            out.push_str(&expr_text(length));
632            out.push(')');
633            out
634        }
635    }
636}
637
638fn literal_text(literal: &Literal) -> String {
639    match literal {
640        Literal::Int(text) | Literal::Float(text) => text.clone(),
641        Literal::Bool(value) => {
642            if *value {
643                "true".to_string()
644            } else {
645                "false".to_string()
646            }
647        }
648        // A Go rune literal is a code point and not an octet, so a
649        // `char` above the execution set is still one literal. The
650        // numeric form is written rather than the quoted one: a rune
651        // literal's type is `rune`, which is `int32`, and an IDL `char`
652        // is one octet — the conversion the member's type asks for is
653        // then explicit at the declaration rather than implicit in a
654        // literal that happens to fit.
655        Literal::Char(ch) => alloc::format!("{}", u32::from(*ch)),
656        Literal::Str(text) => {
657            let mut out = String::from("\"");
658            out.push_str(&escape_string(text));
659            out.push('"');
660            out
661        }
662        Literal::Bytes(bytes) => {
663            let mut out = String::from("[]byte{");
664            let parts: Vec<String> = bytes.iter().map(|b| alloc::format!("0x{b:02X}")).collect();
665            out.push_str(&parts.join(", "));
666            out.push('}');
667            out
668        }
669        Literal::Null => "nil".to_string(),
670    }
671}
672
673fn doc(w: &mut Writer, doc: &Doc) {
674    if doc.is_empty() {
675        return;
676    }
677    comment_lines(w, "// ", doc.as_lines());
678}
679
680/// The identifier a name renders as, for a caller assembling one.
681#[must_use]
682pub fn ident(name: &Ident) -> String {
683    name.as_str().to_string()
684}