Skip to main content

ferogram_tl_gen/
codegen.rs

1/*
2 * Copyright (c) 2026 Ankit Chaubey <ankitchaubey.dev@gmail.com>
3 * https://github.com/ankit-chaubey
4 *
5 * Project: ferogram
6 * Website: https://ferogram.dev
7 *
8 * Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
9 * https://www.apache.org/licenses/LICENSE-2.0> or the MIT license
10 * <LICENSE-MIT or https://opensource.org/licenses/MIT>, at your option.
11 * This file may not be copied, modified, or distributed except according
12 * to those terms.
13 */
14
15use std::collections::HashMap;
16use std::fs::File;
17use std::io::{self, Write};
18use std::path::Path;
19
20use ferogram_tl_parser::tl::{Category, Definition, ParameterType};
21
22use crate::grouper;
23use crate::metadata::Metadata;
24use crate::namegen as n;
25
26/// Generation configuration.
27pub struct Config {
28    /// Emit `name_for_id(id) -> Option<&'static str>` in the common module.
29    pub gen_name_for_id: bool,
30    /// Also implement `Deserializable` for function types (useful for servers).
31    pub deserializable_functions: bool,
32    /// Derive `Debug` on all generated types.
33    pub impl_debug: bool,
34    /// Emit `From<types::Foo> for enums::Bar` impls.
35    pub impl_from_type: bool,
36    /// Emit `TryFrom<enums::Bar> for types::Foo` impls.
37    pub impl_from_enum: bool,
38    /// Derive `serde::{Serialize, Deserialize}` on all types.
39    pub impl_serde: bool,
40    /// Emit `.field()` accessors on enums whose variants share a field name
41    /// and type (see `write_enum_accessors`).
42    pub gen_field_accessors: bool,
43}
44
45impl Default for Config {
46    fn default() -> Self {
47        Self {
48            gen_name_for_id: false,
49            deserializable_functions: false,
50            impl_debug: true,
51            impl_from_type: true,
52            impl_from_enum: true,
53            impl_serde: false,
54            gen_field_accessors: true,
55        }
56    }
57}
58
59// Outputs
60
61/// Writers for each generated Rust module.
62pub struct Outputs<W: Write> {
63    /// Receives the layer constant, `name_for_id`, etc.
64    pub common: W,
65    /// Receives `pub mod types { ... }` (concrete constructors as structs).
66    pub types: W,
67    /// Receives `pub mod functions { ... }` (RPC functions as structs).
68    pub functions: W,
69    /// Receives `pub mod enums { ... }` (boxed types as enums).
70    pub enums: W,
71}
72
73impl Outputs<File> {
74    /// Convenience constructor that opens files inside `out_dir`.
75    pub fn from_dir(out_dir: &str) -> io::Result<Self> {
76        let p = Path::new(out_dir);
77        Ok(Self {
78            common: File::create(p.join("generated_common.rs"))?,
79            types: File::create(p.join("generated_types.rs"))?,
80            functions: File::create(p.join("generated_functions.rs"))?,
81            enums: File::create(p.join("generated_enums.rs"))?,
82        })
83    }
84}
85
86impl<W: Write> Outputs<W> {
87    /// Flush all writers.
88    pub fn flush(&mut self) -> io::Result<()> {
89        self.common.flush()?;
90        self.types.flush()?;
91        self.functions.flush()?;
92        self.enums.flush()
93    }
94}
95
96// Special-cased primitives
97
98/// These TL types are handled as Rust primitives; we never emit structs/enums.
99const BUILTIN_TYPES: &[&str] = &["Bool", "True"];
100
101fn is_builtin(ty_name: &str) -> bool {
102    BUILTIN_TYPES.contains(&ty_name)
103}
104
105// Public API
106
107/// Generate Rust source code from a slice of parsed TL definitions.
108///
109/// Write results into `outputs`. Call `outputs.flush()` when done.
110pub fn generate<W: Write>(
111    defs: &[Definition],
112    config: &Config,
113    outputs: &mut Outputs<W>,
114) -> io::Result<()> {
115    let meta = Metadata::build(defs);
116
117    write_common(defs, config, &mut outputs.common)?;
118    write_types_mod(defs, config, &meta, &mut outputs.types)?;
119    write_functions_mod(defs, config, &meta, &mut outputs.functions)?;
120    write_enums_mod(defs, config, &meta, &mut outputs.enums)?;
121
122    Ok(())
123}
124
125// Common module
126
127fn write_common<W: Write>(defs: &[Definition], config: &Config, out: &mut W) -> io::Result<()> {
128    // Extract LAYER constant from the first `// LAYER N` comment heuristic
129    // for now we derive it from the highest layer seen in definitions or emit 0.
130    writeln!(out, "// @generated: do not edit by hand")?;
131    writeln!(out, "// Re-run the build script to regenerate.")?;
132    writeln!(out)?;
133    writeln!(out, "/// The API layer this code was generated from.")?;
134    writeln!(out, "pub const LAYER: i32 = 0; // update via build.rs")?;
135    writeln!(out)?;
136
137    if config.gen_name_for_id {
138        writeln!(out, "/// Returns the TL name for a known constructor ID.")?;
139        writeln!(
140            out,
141            "pub fn name_for_id(id: u32) -> Option<&'static str> {{"
142        )?;
143        writeln!(out, "    match id {{")?;
144        for def in defs {
145            writeln!(
146                out,
147                "        {:#010x} => Some(\"{}\"),",
148                def.id,
149                def.full_name()
150            )?;
151        }
152        writeln!(out, "        _ => None,")?;
153        writeln!(out, "    }}")?;
154        writeln!(out, "}}")?;
155    }
156
157    Ok(())
158}
159
160// Struct generation (types + functions)
161
162fn write_types_mod<W: Write>(
163    defs: &[Definition],
164    config: &Config,
165    meta: &Metadata,
166    out: &mut W,
167) -> io::Result<()> {
168    writeln!(out, "// @generated: do not edit by hand")?;
169    writeln!(out, "pub mod types {{")?;
170
171    let grouped = grouper::group_by_ns(defs, Category::Types);
172    let mut namespaces: Vec<&String> = grouped.keys().collect();
173    namespaces.sort();
174
175    for ns in namespaces {
176        let bucket = &grouped[ns];
177        let indent: String = if ns.is_empty() {
178            "    ".to_owned()
179        } else {
180            writeln!(out, "    pub mod {ns} {{")?;
181            "        ".to_owned()
182        };
183
184        for def in bucket {
185            write_struct(out, &indent, def, meta, config)?;
186            write_identifiable(out, &indent, def)?;
187            write_struct_serializable(out, &indent, def, meta)?;
188            write_struct_deserializable(out, &indent, def, meta)?;
189        }
190
191        if !ns.is_empty() {
192            writeln!(out, "    }}")?;
193        }
194    }
195
196    writeln!(out, "}}")
197}
198
199fn write_functions_mod<W: Write>(
200    defs: &[Definition],
201    config: &Config,
202    meta: &Metadata,
203    out: &mut W,
204) -> io::Result<()> {
205    writeln!(out, "// @generated: do not edit by hand")?;
206    writeln!(out, "pub mod functions {{")?;
207
208    let grouped = grouper::group_by_ns(defs, Category::Functions);
209    let mut namespaces: Vec<&String> = grouped.keys().collect();
210    namespaces.sort();
211
212    for ns in namespaces {
213        let bucket = &grouped[ns];
214        let indent: String = if ns.is_empty() {
215            "    ".to_owned()
216        } else {
217            writeln!(out, "    pub mod {ns} {{")?;
218            "        ".to_owned()
219        };
220
221        for def in bucket {
222            write_struct(out, &indent, def, meta, config)?;
223            write_identifiable(out, &indent, def)?;
224            write_struct_serializable(out, &indent, def, meta)?;
225            if config.deserializable_functions {
226                write_struct_deserializable(out, &indent, def, meta)?;
227            }
228            write_remote_call(out, &indent, def, meta)?;
229        }
230
231        if !ns.is_empty() {
232            writeln!(out, "    }}")?;
233        }
234    }
235
236    writeln!(out, "}}")
237}
238
239// Struct pieces
240
241fn generic_list(def: &Definition, bounds: &str) -> String {
242    let mut params: Vec<&str> = Vec::new();
243    for p in &def.params {
244        if let ParameterType::Normal { ty, .. } = &p.ty
245            && ty.generic_ref
246            && !params.contains(&ty.name.as_str())
247        {
248            params.push(&ty.name);
249        }
250    }
251    if params.is_empty() {
252        String::new()
253    } else {
254        format!("<{}>", params.join(&format!("{bounds}, ")) + bounds)
255    }
256}
257
258fn write_struct<W: Write>(
259    out: &mut W,
260    indent: &str,
261    def: &Definition,
262    meta: &Metadata,
263    config: &Config,
264) -> io::Result<()> {
265    let kind = match def.category {
266        Category::Types => "constructor",
267        Category::Functions => "method",
268    };
269    writeln!(
270        out,
271        "\n{indent}/// [`{name}`](https://core.telegram.org/{kind}/{name})\n\
272         {indent}///\n\
273         {indent}/// Generated from:\n\
274         {indent}/// ```tl\n\
275         {indent}/// {def}\n\
276         {indent}/// ```",
277        name = def.full_name(),
278    )?;
279
280    if config.impl_debug {
281        writeln!(out, "{indent}#[derive(Debug)]")?;
282    }
283    if config.impl_serde {
284        writeln!(
285            out,
286            "{indent}#[derive(serde::Serialize, serde::Deserialize)]"
287        )?;
288    }
289    writeln!(out, "{indent}#[derive(Clone, PartialEq)]")?;
290    writeln!(
291        out,
292        "{indent}pub struct {}{} {{",
293        n::def_type_name(def),
294        generic_list(def, ""),
295    )?;
296
297    for param in &def.params {
298        match &param.ty {
299            ParameterType::Flags => {} // computed on-the-fly
300            ParameterType::Normal { .. } => {
301                writeln!(
302                    out,
303                    "{indent}    pub {}: {},",
304                    n::param_attr_name(param),
305                    n::param_qual_name(param, meta),
306                )?;
307            }
308        }
309    }
310    writeln!(out, "{indent}}}")
311}
312
313fn write_identifiable<W: Write>(out: &mut W, indent: &str, def: &Definition) -> io::Result<()> {
314    let gl = generic_list(def, "");
315    writeln!(
316        out,
317        "{indent}impl{gl} crate::Identifiable for {}{gl} {{\n\
318         {indent}    const CONSTRUCTOR_ID: u32 = {:#010x};\n\
319         {indent}}}",
320        n::def_type_name(def),
321        def.id,
322    )
323}
324
325fn write_struct_serializable<W: Write>(
326    out: &mut W,
327    indent: &str,
328    def: &Definition,
329    meta: &Metadata,
330) -> io::Result<()> {
331    let gl_decl = generic_list(def, ": crate::Serializable");
332    let gl_use = generic_list(def, "");
333
334    writeln!(
335        out,
336        "{indent}impl{gl_decl} crate::Serializable for {}{gl_use} {{",
337        n::def_type_name(def)
338    )?;
339
340    let underscore = if def.category == Category::Types && def.params.is_empty() {
341        "_"
342    } else {
343        ""
344    };
345    writeln!(
346        out,
347        "{indent}    fn serialize(&self, {underscore}buf: &mut impl Extend<u8>) {{"
348    )?;
349
350    if def.category == Category::Functions {
351        writeln!(out, "{indent}        use crate::Identifiable;")?;
352        writeln!(out, "{indent}        Self::CONSTRUCTOR_ID.serialize(buf);")?;
353    }
354
355    for param in &def.params {
356        write_param_serialization(out, indent, def, meta, param)?;
357    }
358
359    writeln!(out, "{indent}    }}")?;
360    writeln!(out, "{indent}}}")
361}
362
363fn write_param_serialization<W: Write>(
364    out: &mut W,
365    indent: &str,
366    def: &Definition,
367    meta: &Metadata,
368    param: &ferogram_tl_parser::tl::Parameter,
369) -> io::Result<()> {
370    use ParameterType::*;
371
372    match &param.ty {
373        Flags => {
374            if meta.is_unused_flag(def, param) {
375                writeln!(out, "{indent}        0u32.serialize(buf);")?;
376                return Ok(());
377            }
378            // Compute the flags bitmask from optional params
379            write!(out, "{indent}        (")?;
380            let mut first = true;
381            for other in &def.params {
382                if let Normal {
383                    flag: Some(fl), ty, ..
384                } = &other.ty
385                {
386                    if fl.name != param.name {
387                        continue;
388                    }
389                    if !first {
390                        write!(out, " | ")?;
391                    }
392                    first = false;
393                    if ty.name == "true" {
394                        write!(
395                            out,
396                            "if self.{} {{ 1 << {} }} else {{ 0 }}",
397                            n::param_attr_name(other),
398                            fl.index
399                        )?;
400                    } else {
401                        write!(
402                            out,
403                            "if self.{}.is_some() {{ 1 << {} }} else {{ 0 }}",
404                            n::param_attr_name(other),
405                            fl.index
406                        )?;
407                    }
408                }
409            }
410            if first {
411                write!(out, "0u32")?;
412            }
413            writeln!(out, ").serialize(buf);")?;
414        }
415        Normal { ty, flag } => {
416            let attr = n::param_attr_name(param);
417            if flag.is_some() {
418                if ty.name == "true" {
419                    // bool flag: nothing to serialize, it's in the flags word
420                } else {
421                    writeln!(
422                        out,
423                        "{indent}        if let Some(v) = &self.{attr} {{ v.serialize(buf); }}"
424                    )?;
425                }
426            } else {
427                writeln!(out, "{indent}        self.{attr}.serialize(buf);")?;
428            }
429        }
430    }
431    Ok(())
432}
433
434fn write_struct_deserializable<W: Write>(
435    out: &mut W,
436    indent: &str,
437    def: &Definition,
438    meta: &Metadata,
439) -> io::Result<()> {
440    let gl_decl = generic_list(def, ": crate::Deserializable");
441    let gl_use = generic_list(def, "");
442
443    // Empty structs never read from `buf`. Name it `_buf` to suppress the
444    // unused-variable warning in the generated output.
445    let buf_name = if def.params.is_empty() { "_buf" } else { "buf" };
446
447    writeln!(
448        out,
449        "{indent}impl{gl_decl} crate::Deserializable for {}{gl_use} {{",
450        n::def_type_name(def)
451    )?;
452    writeln!(
453        out,
454        "{indent}    fn deserialize({buf_name}: crate::deserialize::Buffer) -> crate::deserialize::Result<Self> {{"
455    )?;
456
457    // Debug: entry banner (only for non-empty structs; empty ones use _buf)
458    let struct_name = n::def_type_name(def);
459    if !def.params.is_empty() {
460        writeln!(
461            out,
462            "{indent}        if crate::deserialize::tl_debug() {{ \
463             eprintln!(\"[TL] >>  {}::deserialize  pos={{}}\", buf.pos()); }}",
464            struct_name
465        )?;
466    }
467
468    // Deserialize params in exact TL schema order.
469    // Flags fields are read inline where they appear - never hoisted.
470    for param in &def.params {
471        match &param.ty {
472            ParameterType::Flags => {
473                let fp_attr = n::param_attr_name(param);
474                writeln!(
475                    out,
476                    "{indent}        if crate::deserialize::tl_debug() {{ \
477                     eprintln!(\"[TL]   {struct_name}.{fp_attr} (flags) pos={{}}\", buf.pos()); }}"
478                )?;
479                writeln!(
480                    out,
481                    "{indent}        let _{fp_attr} = u32::deserialize(buf)?;"
482                )?;
483                writeln!(
484                    out,
485                    "{indent}        if crate::deserialize::tl_debug() {{ \
486                     eprintln!(\"[TL]   {struct_name}.{fp_attr} = {{:#034b}}  pos={{}}\", _{fp_attr}, buf.pos()); }}"
487                )?;
488            }
489            ParameterType::Normal { ty, flag } => {
490                let attr = n::param_attr_name(param);
491                // before
492                writeln!(
493                    out,
494                    "{indent}        if crate::deserialize::tl_debug() {{ \
495                     eprintln!(\"[TL]   {struct_name}.{attr} pos={{}}\", buf.pos()); }}"
496                )?;
497                if let Some(fl) = flag {
498                    if ty.name == "true" {
499                        writeln!(
500                            out,
501                            "{indent}        let {attr} = (_{} & (1 << {})) != 0;",
502                            fl.name, fl.index
503                        )?;
504                    } else {
505                        writeln!(
506                            out,
507                            "{indent}        let {attr} = if (_{} & (1 << {})) != 0 {{ Some({}::deserialize(buf)?) }} else {{ None }};",
508                            fl.name,
509                            fl.index,
510                            n::type_item_path(ty, meta)
511                        )?;
512                    }
513                } else {
514                    writeln!(
515                        out,
516                        "{indent}        let {attr} = {}::deserialize(buf)?;",
517                        n::type_item_path(ty, meta)
518                    )?;
519                }
520                // after
521                writeln!(
522                    out,
523                    "{indent}        if crate::deserialize::tl_debug() {{ \
524                     eprintln!(\"[TL]   {struct_name}.{attr} done  pos={{}}\", buf.pos()); }}"
525                )?;
526            }
527        }
528    }
529
530    // Debug: exit banner (only for non-empty structs; empty ones use _buf)
531    if !def.params.is_empty() {
532        writeln!(
533            out,
534            "{indent}        if crate::deserialize::tl_debug() {{ \
535             eprintln!(\"[TL] <<  {struct_name}::deserialize done  pos={{}}\", buf.pos()); }}"
536        )?;
537    }
538
539    writeln!(out, "{indent}        Ok(Self {{")?;
540    for param in &def.params {
541        if param.ty != ParameterType::Flags {
542            let attr = n::param_attr_name(param);
543            writeln!(out, "{indent}            {attr},")?;
544        }
545    }
546    writeln!(out, "{indent}        }})")?;
547    writeln!(out, "{indent}    }}")?;
548    writeln!(out, "{indent}}}")
549}
550
551fn write_remote_call<W: Write>(
552    out: &mut W,
553    indent: &str,
554    def: &Definition,
555    meta: &Metadata,
556) -> io::Result<()> {
557    // Generic functions (e.g. invokeWithLayer<X>) need the type parameter on
558    // the impl header and on the struct name, just like every other write_* helper.
559    let gl_decl = generic_list(def, ": crate::Serializable + crate::Deserializable");
560    let gl_use = generic_list(def, "");
561    writeln!(
562        out,
563        "{indent}impl{gl_decl} crate::RemoteCall for {}{gl_use} {{",
564        n::def_type_name(def)
565    )?;
566    writeln!(
567        out,
568        "{indent}    type Return = {};",
569        n::type_qual_name(&def.ty, meta)
570    )?;
571    writeln!(out, "{indent}}}")
572}
573
574// Enum generation
575
576fn write_enums_mod<W: Write>(
577    defs: &[Definition],
578    config: &Config,
579    meta: &Metadata,
580    out: &mut W,
581) -> io::Result<()> {
582    writeln!(out, "// @generated: do not edit by hand")?;
583    writeln!(out, "pub mod enums {{")?;
584
585    let grouped = grouper::group_types_by_ns(defs);
586    let mut keys: Vec<&Option<String>> = grouped.keys().collect();
587    keys.sort();
588
589    for key in keys {
590        let types = &grouped[key];
591        let indent = if let Some(ns) = key {
592            writeln!(out, "    pub mod {ns} {{")?;
593            "        ".to_owned()
594        } else {
595            "    ".to_owned()
596        };
597
598        for ty in types.iter().filter(|t| !is_builtin(&t.name)) {
599            write_enum(out, &indent, ty, meta, config)?;
600            write_enum_serializable(out, &indent, ty, meta)?;
601            write_enum_deserializable(out, &indent, ty, meta)?;
602            if config.impl_from_type {
603                write_impl_from(out, &indent, ty, meta)?;
604            }
605            if config.impl_from_enum {
606                write_impl_try_from(out, &indent, ty, meta)?;
607            }
608            if config.gen_field_accessors {
609                write_enum_accessors(out, &indent, ty, meta)?;
610            }
611        }
612
613        if key.is_some() {
614            writeln!(out, "    }}")?;
615        }
616    }
617
618    writeln!(out, "}}")
619}
620
621fn write_enum<W: Write>(
622    out: &mut W,
623    indent: &str,
624    ty: &ferogram_tl_parser::tl::Type,
625    meta: &Metadata,
626    config: &Config,
627) -> io::Result<()> {
628    writeln!(
629        out,
630        "\n{indent}/// [`{name}`](https://core.telegram.org/type/{name})",
631        name = n::type_name(ty)
632    )?;
633    if config.impl_debug {
634        writeln!(out, "{indent}#[derive(Debug)]")?;
635    }
636    if config.impl_serde {
637        writeln!(
638            out,
639            "{indent}#[derive(serde::Serialize, serde::Deserialize)]"
640        )?;
641    }
642    writeln!(out, "{indent}#[derive(Clone, PartialEq)]")?;
643    writeln!(out, "{indent}pub enum {} {{", n::type_name(ty))?;
644
645    for def in meta.defs_for_type(ty) {
646        let variant = n::def_variant_name(def);
647        if def.params.is_empty() {
648            writeln!(out, "{indent}    {variant},")?;
649        } else if meta.is_recursive(def) {
650            writeln!(
651                out,
652                "{indent}    {variant}(Box<{}>),",
653                n::def_qual_name(def)
654            )?;
655        } else {
656            writeln!(out, "{indent}    {variant}({}),", n::def_qual_name(def))?;
657        }
658    }
659
660    writeln!(out, "{indent}}}")
661}
662
663fn write_enum_serializable<W: Write>(
664    out: &mut W,
665    indent: &str,
666    ty: &ferogram_tl_parser::tl::Type,
667    meta: &Metadata,
668) -> io::Result<()> {
669    writeln!(
670        out,
671        "{indent}impl crate::Serializable for {} {{",
672        n::type_name(ty)
673    )?;
674    writeln!(
675        out,
676        "{indent}    fn serialize(&self, buf: &mut impl Extend<u8>) {{"
677    )?;
678    writeln!(out, "{indent}        use crate::Identifiable;")?;
679    writeln!(out, "{indent}        match self {{")?;
680
681    for def in meta.defs_for_type(ty) {
682        let variant = n::def_variant_name(def);
683        let bind = if def.params.is_empty() { "" } else { "(x)" };
684        writeln!(out, "{indent}            Self::{variant}{bind} => {{")?;
685        writeln!(
686            out,
687            "{indent}                {}::CONSTRUCTOR_ID.serialize(buf);",
688            n::def_qual_name(def)
689        )?;
690        if !def.params.is_empty() {
691            writeln!(out, "{indent}                x.serialize(buf);")?;
692        }
693        writeln!(out, "{indent}            }}")?;
694    }
695
696    writeln!(out, "{indent}        }}")?;
697    writeln!(out, "{indent}    }}")?;
698    writeln!(out, "{indent}}}")
699}
700
701fn write_enum_deserializable<W: Write>(
702    out: &mut W,
703    indent: &str,
704    ty: &ferogram_tl_parser::tl::Type,
705    meta: &Metadata,
706) -> io::Result<()> {
707    writeln!(
708        out,
709        "{indent}impl crate::Deserializable for {} {{",
710        n::type_name(ty)
711    )?;
712    writeln!(
713        out,
714        "{indent}    fn deserialize(buf: crate::deserialize::Buffer) -> crate::deserialize::Result<Self> {{"
715    )?;
716    let enum_name = n::type_name(ty);
717    writeln!(out, "{indent}        use crate::Identifiable;")?;
718    writeln!(out, "{indent}        let id = u32::deserialize(buf)?;")?;
719    writeln!(
720        out,
721        "{indent}        if crate::deserialize::tl_debug() {{ \
722         eprintln!(\"[TL] ENUM  {enum_name}  ctor={{:#010x}}  pos={{}}\", id, buf.pos()); }}"
723    )?;
724    writeln!(out, "{indent}        Ok(match id {{")?;
725
726    for def in meta.defs_for_type(ty) {
727        let variant = n::def_variant_name(def);
728        let qual = n::def_qual_name(def);
729        if def.params.is_empty() {
730            writeln!(
731                out,
732                "{indent}            {qual}::CONSTRUCTOR_ID => Self::{variant},"
733            )?;
734        } else if meta.is_recursive(def) {
735            writeln!(
736                out,
737                "{indent}            {qual}::CONSTRUCTOR_ID => Self::{variant}(Box::new({qual}::deserialize(buf)?)),"
738            )?;
739        } else {
740            writeln!(
741                out,
742                "{indent}            {qual}::CONSTRUCTOR_ID => Self::{variant}({qual}::deserialize(buf)?),"
743            )?;
744        }
745    }
746
747    writeln!(
748        out,
749        "{indent}            _ => return Err(crate::deserialize::Error::UnexpectedConstructor {{ id }}),"
750    )?;
751    writeln!(out, "{indent}        }})")?;
752    writeln!(out, "{indent}    }}")?;
753    writeln!(out, "{indent}}}")
754}
755
756fn write_impl_from<W: Write>(
757    out: &mut W,
758    indent: &str,
759    ty: &ferogram_tl_parser::tl::Type,
760    meta: &Metadata,
761) -> io::Result<()> {
762    for def in meta.defs_for_type(ty) {
763        let enum_name = n::type_name(ty);
764        let qual = n::def_qual_name(def);
765        let variant = n::def_variant_name(def);
766
767        writeln!(out, "{indent}impl From<{qual}> for {enum_name} {{")?;
768        let underscore = if def.params.is_empty() { "_" } else { "" };
769        writeln!(out, "{indent}    fn from({underscore}x: {qual}) -> Self {{")?;
770        if def.params.is_empty() {
771            writeln!(out, "{indent}        Self::{variant}")?;
772        } else if meta.is_recursive(def) {
773            writeln!(out, "{indent}        Self::{variant}(Box::new(x))")?;
774        } else {
775            writeln!(out, "{indent}        Self::{variant}(x)")?;
776        }
777        writeln!(out, "{indent}    }}")?;
778        writeln!(out, "{indent}}}")?;
779    }
780    Ok(())
781}
782
783fn write_impl_try_from<W: Write>(
784    out: &mut W,
785    indent: &str,
786    ty: &ferogram_tl_parser::tl::Type,
787    meta: &Metadata,
788) -> io::Result<()> {
789    let enum_name = n::type_name(ty);
790    for def in meta.defs_for_type(ty) {
791        if def.params.is_empty() {
792            continue;
793        }
794        let qual = n::def_qual_name(def);
795        let variant = n::def_variant_name(def);
796
797        writeln!(out, "{indent}impl TryFrom<{enum_name}> for {qual} {{")?;
798        writeln!(out, "{indent}    type Error = {enum_name};")?;
799        writeln!(out, "{indent}    #[allow(unreachable_patterns)]")?;
800        writeln!(
801            out,
802            "{indent}    fn try_from(v: {enum_name}) -> Result<Self, Self::Error> {{"
803        )?;
804        writeln!(out, "{indent}        match v {{")?;
805        if meta.is_recursive(def) {
806            writeln!(
807                out,
808                "{indent}            {enum_name}::{variant}(x) => Ok(*x),"
809            )?;
810        } else {
811            writeln!(
812                out,
813                "{indent}            {enum_name}::{variant}(x) => Ok(x),"
814            )?;
815        }
816        writeln!(out, "{indent}            other => Err(other),")?;
817        writeln!(out, "{indent}        }}")?;
818        writeln!(out, "{indent}    }}")?;
819        writeln!(out, "{indent}}}")?;
820    }
821    Ok(())
822}
823
824// Field accessor generation
825//
826// For an enum type with several constructors, look for parameter names that
827// resolve to the same Rust type across variants and emit a `.field()`
828// accessor. This is scoped narrowly on purpose:
829//
830// - A field name only qualifies if every variant that defines it agrees on
831//   the exact Rust type. Any mismatch drops that field entirely, no partial
832//   generation, no coercion.
833// - If the field is present (optionally behind a flag) in every variant, the
834//   accessor returns `&Ty` and the match is exhaustive.
835// - If only a subset of variants carry it, the accessor returns `Option<&Ty>`
836//   with a `_ => None` arm covering the rest.
837// - Fields whose type touches a generic parameter are skipped, since the
838//   accessor lives on the (non-generic) enum itself.
839// - This does not attempt struct-like restructuring across mismatched shapes
840//   (e.g. collapsing a `name`/`site_name` pair into one field) that stays
841//   hand-written.
842
843/// Per-field bookkeeping while scanning a type's constructors.
844struct FieldAgg {
845    /// Rust type once, `None` before the first sighting.
846    rust_type: Option<String>,
847    /// Set once two variants disagree on `rust_type`; excludes the field.
848    mismatched: bool,
849    /// One slot per constructor, in the same order as `defs_for_type`.
850    /// `Some(is_flag_optional)` if that constructor carries the field.
851    presence: Vec<Option<bool>>,
852}
853
854/// True if `ty` (or any nested generic argument) refers to a generic
855/// parameter, meaning it can't be named from outside the constructor.
856fn type_is_generic(ty: &ferogram_tl_parser::tl::Type) -> bool {
857    ty.generic_ref
858        || ty
859            .generic_arg
860            .as_deref()
861            .map(type_is_generic)
862            .unwrap_or(false)
863}
864
865fn write_enum_accessors<W: Write>(
866    out: &mut W,
867    indent: &str,
868    ty: &ferogram_tl_parser::tl::Type,
869    meta: &Metadata,
870) -> io::Result<()> {
871    let defs = meta.defs_for_type(ty);
872    if defs.len() < 2 {
873        return Ok(());
874    }
875
876    // Insertion-ordered field names so output stays stable across runs
877    // (HashMap iteration order isn't).
878    let mut order: Vec<String> = Vec::new();
879    let mut fields: HashMap<String, FieldAgg> = HashMap::new();
880
881    for (i, def) in defs.iter().enumerate() {
882        for param in &def.params {
883            let ParameterType::Normal { ty: pty, flag } = &param.ty else {
884                continue;
885            };
886            if type_is_generic(pty) {
887                continue;
888            }
889
890            let field = n::param_attr_name(param);
891            let (rust_type, is_optional) = if flag.is_some() && pty.name == "true" {
892                ("bool".to_owned(), false)
893            } else {
894                (n::type_qual_name(pty, meta), flag.is_some())
895            };
896
897            let agg = fields.entry(field.clone()).or_insert_with(|| {
898                order.push(field.clone());
899                FieldAgg {
900                    rust_type: None,
901                    mismatched: false,
902                    presence: vec![None; defs.len()],
903                }
904            });
905
906            match &agg.rust_type {
907                None => agg.rust_type = Some(rust_type),
908                Some(existing) if *existing != rust_type => agg.mismatched = true,
909                _ => {}
910            }
911            agg.presence[i] = Some(is_optional);
912        }
913    }
914
915    let mut emitted_header = false;
916
917    for field in &order {
918        let agg = &fields[field];
919        if agg.mismatched {
920            continue;
921        }
922        let Some(rust_type) = &agg.rust_type else {
923            continue;
924        };
925
926        // Not worth a match statement over a single variant.
927        let present_count = agg.presence.iter().filter(|p| p.is_some()).count();
928        if present_count < 2 {
929            continue;
930        }
931
932        let all_present = present_count == defs.len();
933        let any_optional = agg.presence.iter().any(|p| matches!(p, Some(true)));
934        // Guaranteed on every variant and never behind a flag: skip the
935        // Option wrapper and the catch-all arm entirely.
936        let direct = all_present && !any_optional;
937
938        if !emitted_header {
939            writeln!(out, "\n{indent}impl {} {{", n::type_name(ty))?;
940            emitted_header = true;
941        }
942
943        let ret_ty = if direct {
944            format!("&{rust_type}")
945        } else {
946            format!("Option<&{rust_type}>")
947        };
948
949        writeln!(
950            out,
951            "{indent}    /// Returns `{field}` if present in this variant."
952        )?;
953        writeln!(out, "{indent}    pub fn {field}(&self) -> {ret_ty} {{")?;
954        writeln!(out, "{indent}        match self {{")?;
955
956        for (i, def) in defs.iter().enumerate() {
957            let variant = n::def_variant_name(def);
958            let bind = if def.params.is_empty() { "" } else { "(x)" };
959
960            match agg.presence[i] {
961                Some(true) => writeln!(
962                    out,
963                    "{indent}            Self::{variant}{bind} => x.{field}.as_ref(),"
964                )?,
965                Some(false) => {
966                    let expr = if direct {
967                        format!("&x.{field}")
968                    } else {
969                        format!("Some(&x.{field})")
970                    };
971                    writeln!(out, "{indent}            Self::{variant}{bind} => {expr},")?
972                }
973                None => {} // covered by the wildcard arm below
974            }
975        }
976
977        if !all_present {
978            writeln!(out, "{indent}            _ => None,")?;
979        }
980
981        writeln!(out, "{indent}        }}")?;
982        writeln!(out, "{indent}    }}")?;
983    }
984
985    if emitted_header {
986        writeln!(out, "{indent}}}")?;
987    }
988
989    Ok(())
990}