plotnik_compiler/typegen/typescript/
emitter.rs

1//! Core emitter struct and main emit logic.
2
3use std::collections::hash_map::Entry;
4use std::collections::{BTreeSet, HashMap, HashSet};
5
6use plotnik_bytecode::{EntrypointsView, Module, StringsView, TypeId, TypesView};
7use plotnik_core::Colors;
8
9use super::Config;
10
11/// TypeScript emitter from bytecode module.
12pub struct Emitter<'a> {
13    pub(super) types: TypesView<'a>,
14    pub(super) strings: StringsView<'a>,
15    pub(super) entrypoints: EntrypointsView<'a>,
16    pub(super) config: Config,
17
18    /// TypeId -> assigned name mapping
19    pub(super) type_names: HashMap<TypeId, String>,
20    /// Names already used (for collision avoidance)
21    pub(super) used_names: BTreeSet<String>,
22    /// Track which builtin types are referenced
23    pub(super) node_referenced: bool,
24    /// Track which types have been emitted
25    pub(super) emitted: HashSet<TypeId>,
26    /// Types visited during builtin reference collection (cycle detection)
27    pub(super) refs_visited: HashSet<TypeId>,
28    /// Output buffer
29    pub(super) output: String,
30}
31
32impl<'a> Emitter<'a> {
33    pub fn new(module: &'a Module, config: Config) -> Self {
34        Self {
35            types: module.types(),
36            strings: module.strings(),
37            entrypoints: module.entrypoints(),
38            config,
39            type_names: HashMap::new(),
40            used_names: BTreeSet::new(),
41            node_referenced: false,
42            emitted: HashSet::new(),
43            refs_visited: HashSet::new(),
44            output: String::new(),
45        }
46    }
47
48    pub(super) fn c(&self) -> Colors {
49        self.config.colors
50    }
51
52    /// Emit TypeScript for all entrypoint types.
53    pub fn emit(mut self) -> String {
54        self.prepare_emission();
55
56        // Collect all entrypoints and their result types
57        let mut primary_names: HashMap<TypeId, String> = HashMap::new();
58        let mut aliases: Vec<(String, TypeId)> = Vec::new();
59
60        for i in 0..self.entrypoints.len() {
61            let ep = self.entrypoints.get(i);
62            let name = self.strings.get(ep.name).to_string();
63            let type_id = ep.result_type;
64
65            match primary_names.entry(type_id) {
66                Entry::Vacant(e) => {
67                    e.insert(name);
68                }
69                Entry::Occupied(_) => {
70                    aliases.push((name, type_id));
71                }
72            }
73        }
74
75        // Collect all reachable types starting from entrypoints
76        let mut to_emit = HashSet::new();
77        for i in 0..self.entrypoints.len() {
78            let ep = self.entrypoints.get(i);
79            self.collect_reachable_types(ep.result_type, &mut to_emit);
80        }
81
82        // Emit in topological order
83        for type_id in self.sort_topologically(to_emit) {
84            if let Some(def_name) = primary_names.get(&type_id) {
85                self.emit_type_definition(def_name, type_id);
86            } else {
87                self.emit_generated_or_custom(type_id);
88            }
89        }
90
91        // Emit remaining entrypoints (primitives, arrays, optionals)
92        // These are not in to_emit because collect_reachable_types skips them
93        for (&type_id, name) in &primary_names {
94            if self.emitted.contains(&type_id) {
95                continue;
96            }
97            self.emit_type_definition(name, type_id);
98        }
99
100        // Emit aliases
101        for (alias_name, type_id) in aliases {
102            if let Some(primary_name) = primary_names.get(&type_id) {
103                self.emit_type_alias(&alias_name, primary_name);
104            }
105        }
106
107        // Ensure exactly one trailing newline
108        self.output.truncate(self.output.trim_end().len());
109        self.output.push('\n');
110        self.output
111    }
112}