Skip to main content

lisette_emit/
lib.rs

1mod bindings;
2pub(crate) mod calls;
3mod collectors;
4pub(crate) mod control_flow;
5pub(crate) mod definitions;
6pub(crate) mod expressions;
7mod imports;
8pub(crate) mod names;
9mod output;
10pub(crate) mod patterns;
11pub(crate) mod queries;
12pub(crate) mod statements;
13pub(crate) mod types;
14mod utils;
15
16pub(crate) use bindings::Bindings;
17pub(crate) use calls::go_interop::GoCallStrategy;
18pub(crate) use definitions::enum_layout::EnumLayout;
19pub(crate) use names::go_name;
20pub(crate) use names::go_name::escape_reserved;
21pub(crate) use output::OutputCollector;
22pub(crate) use types::emitter::{ArmPosition, EmitFlags, LineIndex, LoopContext, Position};
23pub(crate) use types::prelude::PreludeType;
24pub(crate) use utils::is_order_sensitive;
25pub(crate) use utils::write_line;
26
27pub use names::go_name::PRELUDE_IMPORT_PATH;
28pub use output::OutputFile;
29
30use rustc_hash::{FxHashMap as HashMap, FxHashSet as HashSet};
31use std::sync::Arc;
32
33use ecow::EcoString;
34use imports::ImportBuilder;
35use syntax::ast::{Generic, Span};
36use syntax::program::{Definition, EmitInput, File, ModuleId, MutationInfo, UnusedInfo};
37use syntax::types::{Symbol, Type};
38
39#[derive(Clone, Debug, Default)]
40pub struct EmitOptions {
41    pub debug: bool,
42}
43
44pub struct TestEmitConfig<'a> {
45    pub definitions: &'a HashMap<Symbol, Definition>,
46    pub module_id: &'a str,
47    pub go_module: &'a str,
48    pub unused: &'a UnusedInfo,
49    pub mutations: &'a MutationInfo,
50    pub ufcs_methods: &'a HashSet<(String, String)>,
51    pub go_package_names: &'a HashMap<String, String>,
52}
53
54struct EmitContext<'a> {
55    definitions: &'a HashMap<Symbol, Definition>,
56    unused: &'a UnusedInfo,
57    mutations: &'a MutationInfo,
58    ufcs_methods: &'a HashSet<(String, String)>,
59    go_package_names: &'a HashMap<String, String>,
60    entry_module: ModuleId,
61    go_module: String,
62    options: EmitOptions,
63    /// file_id -> byte offset to line lookup.
64    line_indexes: Arc<HashMap<u32, LineIndex>>,
65}
66
67struct ModuleData {
68    make_functions: HashMap<String, String>,
69    enum_layouts: HashMap<String, EnumLayout>,
70    /// Fields that were exported due to serialization tags (e.g. `#[json]`).
71    /// Key is "TypeId.field_name". Checked during field access to match
72    /// the capitalization used in the struct definition.
73    tag_exported_fields: HashSet<String>,
74    /// Method names that appear in any pub interface.
75    /// These must be capitalized in Go to satisfy the interface,
76    /// regardless of the concrete method's own visibility.
77    exported_method_names: HashSet<String>,
78    /// Bounds from constrained impl blocks, keyed by receiver name.
79    /// Go requires type parameter constraints on the type definition itself,
80    /// so we pre-scan impl blocks and merge their bounds into struct generics.
81    impl_bounds: HashMap<String, Vec<Generic>>,
82    /// Types that have unconstrained impl blocks (impl<T> Type<T> with no bounds).
83    /// Used to detect when a type has both constrained and unconstrained impl blocks.
84    unconstrained_impl_receivers: HashSet<String>,
85    /// Maps module IDs to their import aliases (e.g., "lib" → "L", "models/user" → "user").
86    /// Used when emitting cross-module references to use the correct alias.
87    module_aliases: HashMap<String, String>,
88    /// Reverse of `module_aliases`: maps alias → module_id (e.g., "L" → "lib").
89    /// Used for O(1) lookup when resolving an alias back to a module name.
90    reverse_module_aliases: HashMap<String, String>,
91    /// Generic type parameters whose Ref has been absorbed into the Go type parameter.
92    /// When a function has `item: Ref<T>` where T has interface bounds, Go requires
93    /// T itself (not *T) to satisfy the interface. So we emit `item T` and let Go
94    /// infer T = *ConcreteType. Ref<T> for these params should emit as just T.
95    absorbed_ref_generics: HashSet<String>,
96    /// Pre-computed wrapping strategy per Go function.
97    go_call_strategies: HashMap<String, GoCallStrategy>,
98}
99
100struct ScopeState {
101    next_var: usize,
102    bindings: Bindings,
103    /// Stack of Go variable names declared at each scope level.
104    declared: Vec<HashSet<String>>,
105    /// Current Go block scope depth (0 = function level).
106    scope_depth: usize,
107    /// Stack of loop contexts (result var + optional label) per nesting level.
108    loop_stack: Vec<LoopContext>,
109    /// Go variable names currently used as block-to-var assign targets.
110    assign_targets: HashSet<String>,
111    /// Go identifiers emitted as `const` (not `var`).
112    go_const_bindings: HashSet<String>,
113}
114
115impl ScopeState {
116    fn reset_for_top_level(&mut self) {
117        self.next_var = 0;
118        self.bindings.reset();
119        self.declared.clear();
120        self.declared.push(HashSet::default());
121    }
122}
123
124pub struct Emitter<'a> {
125    ctx: EmitContext<'a>,
126    module: ModuleData,
127    scope: ScopeState,
128
129    current_module: ModuleId,
130
131    synthesized_adapter_types: HashMap<(EcoString, EcoString), String>,
132    pending_adapter_types: Vec<String>,
133
134    // Per-file accumulated state (reset between files)
135    flags: EmitFlags,
136    ensure_imported: HashSet<ModuleId>,
137
138    // Temporary emission context (saved/restored per-expression).
139    // These are implicit arguments — ideally parameters, but
140    // plumbing through deep call chains is impractical.
141    position: Position,
142    current_return_context: Option<ReturnContext>,
143    /// Target type for Option/Result assignment (interface coercion).
144    assign_target_ty: Option<Type>,
145    /// Generic function identifiers should NOT add type args when used as callees
146    /// (the call site handles instantiation), only when used as values.
147    emitting_call_callee: bool,
148    /// Set while emitting expressions that will appear in Go `if`/`for`/`switch`
149    /// conditions. Generic composite literals (`Type[Args]{...}`) need inner parens
150    /// in these contexts because gofmt strips outer condition parens for generics.
151    in_condition: bool,
152    /// When true, `emit_regular_call` skips array return wrapping (`arr := call; arr[:]`)
153    /// and returns the raw call string. Set by `emit_go_call_discarded` for discarded calls
154    /// where the array-to-slice conversion is unnecessary.
155    skip_array_return_wrap: bool,
156    /// Declared slot type during tuple staging; recovers Go alias type args that
157    /// call-site inference loses in assign-position match arms.
158    current_slot_expected_ty: Option<Type>,
159    /// Set when the destination is a Go-side function (e.g. a generic
160    /// `func(T) U` callback) that needs the unlowered single-return form.
161    suppress_go_fn_short_circuit: bool,
162}
163
164/// `force_tagged` is set inside try-block IIFEs whose outer signature is
165/// the unwrapped `Result`; their body must return the tagged form even
166/// when `ty` would normally lower.
167#[derive(Clone)]
168pub(crate) struct ReturnContext {
169    pub(crate) ty: Type,
170    pub(crate) force_tagged: bool,
171}
172
173impl ReturnContext {
174    pub(crate) fn new(ty: Type) -> Self {
175        Self {
176            ty,
177            force_tagged: false,
178        }
179    }
180
181    pub(crate) fn tagged(ty: Type) -> Self {
182        Self {
183            ty,
184            force_tagged: true,
185        }
186    }
187}
188
189impl<'a> Emitter<'a> {
190    pub fn emit(analysis: &'a EmitInput, go_module: &str, options: EmitOptions) -> Vec<OutputFile> {
191        let mut output = vec![];
192
193        let line_indexes: Arc<HashMap<u32, LineIndex>> = Arc::new(if options.debug {
194            analysis
195                .files
196                .iter()
197                .map(|(file_id, file)| {
198                    let path = if file.module_id == analysis.entry_module_id {
199                        format!("src/{}", file.name)
200                    } else {
201                        format!("{}/{}", file.module_id, file.name)
202                    };
203                    (*file_id, LineIndex::from_source(path, &file.source))
204                })
205                .collect()
206        } else {
207            HashMap::default()
208        });
209
210        for (module_id, module_info) in &analysis.modules {
211            if analysis.cached_modules.contains(module_id) {
212                continue;
213            }
214
215            let ctx = EmitContext {
216                definitions: &analysis.definitions,
217                unused: &analysis.unused,
218                mutations: &analysis.mutations,
219                ufcs_methods: &analysis.ufcs_methods,
220                go_package_names: &analysis.go_package_names,
221                entry_module: analysis.entry_module_id.to_string(),
222                go_module: go_module.to_string(),
223                options: options.clone(),
224                line_indexes: line_indexes.clone(),
225            };
226            let mut emitter = Self::new(ctx, module_id);
227
228            let files: Vec<_> = module_info
229                .file_ids
230                .iter()
231                .filter_map(|fid| analysis.files.get(fid))
232                .collect();
233
234            let mut module_output = emitter.emit_files(&files, module_id);
235
236            if module_id != &analysis.entry_module_id {
237                for file in &mut module_output {
238                    file.name = format!("{}/{}", module_info.path, file.name);
239                }
240            }
241
242            output.extend(module_output);
243        }
244
245        output
246    }
247
248    pub fn new_for_tests(config: &TestEmitConfig<'a>, source: Option<&str>) -> Self {
249        let (debug, line_indexes) = match source {
250            Some(src) => (
251                true,
252                Arc::new(HashMap::from_iter([(
253                    0u32,
254                    LineIndex::from_source("src/test.lis".to_string(), src),
255                )])),
256            ),
257            None => (false, Arc::new(HashMap::default())),
258        };
259        let ctx = EmitContext {
260            definitions: config.definitions,
261            unused: config.unused,
262            mutations: config.mutations,
263            ufcs_methods: config.ufcs_methods,
264            go_package_names: config.go_package_names,
265            entry_module: config.module_id.to_string(),
266            go_module: config.go_module.to_string(),
267            options: EmitOptions { debug },
268            line_indexes,
269        };
270        Self::new(ctx, config.module_id)
271    }
272
273    fn new(ctx: EmitContext<'a>, current_module: &str) -> Self {
274        Self {
275            ctx,
276            module: ModuleData {
277                make_functions: HashMap::default(),
278                enum_layouts: HashMap::default(),
279                tag_exported_fields: HashSet::default(),
280                exported_method_names: HashSet::default(),
281                impl_bounds: HashMap::default(),
282                unconstrained_impl_receivers: HashSet::default(),
283                module_aliases: HashMap::default(),
284                reverse_module_aliases: HashMap::default(),
285                absorbed_ref_generics: HashSet::default(),
286                go_call_strategies: HashMap::default(),
287            },
288            scope: ScopeState {
289                next_var: 0,
290                bindings: Bindings::new(),
291                declared: vec![HashSet::default()],
292                scope_depth: 0,
293                loop_stack: Vec::new(),
294                assign_targets: HashSet::default(),
295                go_const_bindings: HashSet::default(),
296            },
297            current_module: current_module.to_string(),
298            synthesized_adapter_types: HashMap::default(),
299            pending_adapter_types: Vec::new(),
300            flags: EmitFlags::default(),
301            ensure_imported: HashSet::default(),
302            position: Position::Expression,
303            current_return_context: None,
304            assign_target_ty: None,
305            emitting_call_callee: false,
306            in_condition: false,
307            skip_array_return_wrap: false,
308            current_slot_expected_ty: None,
309            suppress_go_fn_short_circuit: false,
310        }
311    }
312
313    pub(crate) fn emit_condition_operand(
314        &mut self,
315        output: &mut String,
316        expression: &syntax::ast::Expression,
317    ) -> String {
318        let prev = self.in_condition;
319        self.in_condition = true;
320        let result = self.emit_operand(output, expression);
321        self.in_condition = prev;
322        result
323    }
324
325    pub(crate) fn push_loop(&mut self, result_var: impl Into<String>) {
326        self.scope.loop_stack.push(LoopContext {
327            result_var: result_var.into(),
328            label: None,
329        });
330    }
331
332    pub(crate) fn pop_loop(&mut self) {
333        self.scope.loop_stack.pop();
334    }
335
336    pub(crate) fn current_loop_result_var(&self) -> Option<&str> {
337        self.scope
338            .loop_stack
339            .last()
340            .map(|ctx| ctx.result_var.as_str())
341    }
342
343    pub(crate) fn current_loop_label(&self) -> Option<&str> {
344        self.scope
345            .loop_stack
346            .last()
347            .and_then(|ctx| ctx.label.as_deref())
348    }
349
350    pub(crate) fn with_position<F, R>(&mut self, position: Position, f: F) -> R
351    where
352        F: FnOnce(&mut Self) -> R,
353    {
354        let saved = std::mem::replace(&mut self.position, position);
355        let result = f(self);
356        self.position = saved;
357        result
358    }
359
360    pub(crate) fn wrap_value(&self, value: &str) -> String {
361        if value.is_empty() {
362            return String::new();
363        }
364        match &self.position {
365            Position::Tail => format!("return {}\n", value),
366            Position::Statement => format!("{}\n", value),
367            Position::Expression => value.to_string(),
368            Position::Assign(var) => format!("{} = {}\n", var, value),
369        }
370    }
371
372    pub(crate) fn emit_unreachable_if_needed(&self, output: &mut String, has_catchall: bool) {
373        if self.position.is_tail() && !has_catchall {
374            output.push_str("panic(\"unreachable\")\n");
375        }
376    }
377
378    /// Computes the position for match arms based on the current position and result type.
379    ///
380    /// For control flow constructs that need to produce values (match, if-else), this
381    /// determines whether we need a temporary result variable and what position the
382    /// inner branches should use.
383    ///
384    /// If `output` is provided, declares the result variable when needed.
385    pub(crate) fn compute_arm_position(
386        &mut self,
387        output: Option<&mut String>,
388        ty: &Type,
389    ) -> ArmPosition {
390        if self.position.is_tail() {
391            return ArmPosition::from_position(Position::Tail);
392        }
393
394        if let Some(var) = self.position.assign_target() {
395            return ArmPosition::from_position(Position::Assign(var.to_string()));
396        }
397
398        if self.position.is_expression() && !ty.is_unit() {
399            let var = self.fresh_var(Some("result"));
400            if let Some(out) = output {
401                let go_ty = self.go_type_as_string(ty);
402                write_line!(out, "var {} {}", var, go_ty);
403            }
404            return ArmPosition::with_result_var(var);
405        }
406
407        ArmPosition::from_position(Position::Statement)
408    }
409
410    /// Checks if a Go variable name has been declared in the current scope.
411    /// Tracks declarations at each scope level so variable shadowing works correctly.
412    /// Returns true if this is a new declaration (use :=), false if already declared (use =).
413    pub(crate) fn try_declare(&mut self, go_name: &str) -> bool {
414        if let Some(current_scope) = self.scope.declared.last_mut() {
415            if current_scope.contains(go_name) {
416                false
417            } else {
418                current_scope.insert(go_name.to_string());
419                true
420            }
421        } else {
422            true
423        }
424    }
425
426    pub(crate) fn is_declared(&self, go_name: &str) -> bool {
427        self.scope
428            .declared
429            .iter()
430            .any(|scope| scope.contains(go_name))
431    }
432
433    /// Unconditionally marks a Go variable name as declared in the current scope.
434    /// Use this for parameters, which are always "declared" at function entry.
435    pub(crate) fn declare(&mut self, go_name: &str) {
436        if let Some(current_scope) = self.scope.declared.last_mut() {
437            current_scope.insert(go_name.to_string());
438        }
439    }
440
441    pub(crate) fn enter_scope(&mut self) {
442        self.scope.scope_depth += 1;
443        self.scope.bindings.save();
444        self.scope.declared.push(HashSet::default());
445    }
446
447    pub(crate) fn exit_scope(&mut self) {
448        self.scope.scope_depth = self.scope.scope_depth.saturating_sub(1);
449        self.scope.bindings.restore();
450        if self.scope.declared.len() > 1 {
451            self.scope.declared.pop();
452        }
453    }
454
455    pub(crate) fn current_module(&self) -> &str {
456        &self.current_module
457    }
458
459    pub(crate) fn module_alias_for_type(&self, ty: &Type) -> Option<String> {
460        if let Type::Nominal { id, .. } = ty {
461            let module = names::go_name::module_of_type_id(id);
462            self.module.module_aliases.get(module).cloned()
463        } else {
464            None
465        }
466    }
467
468    pub(crate) fn maybe_line_directive(&self, span: &Span) -> String {
469        if !self.ctx.options.debug || span.is_dummy() {
470            return String::new();
471        }
472
473        let Some(source) = self.ctx.line_indexes.get(&span.file_id) else {
474            return String::new();
475        };
476
477        let line = source.line_for_offset(span.byte_offset);
478        let col = source.col_for_offset(span.byte_offset);
479
480        format!("//line {}:{}:{}\n", source.path, line, col)
481    }
482
483    fn unused_imports_for_current_module<'u>(
484        unused: &'u UnusedInfo,
485        current_module: &str,
486    ) -> &'u HashSet<EcoString> {
487        static EMPTY: std::sync::LazyLock<HashSet<EcoString>> =
488            std::sync::LazyLock::new(HashSet::default);
489        unused
490            .imports_by_module
491            .get(current_module)
492            .unwrap_or(&EMPTY)
493    }
494
495    pub fn emit_files(&mut self, files: &[&File], module_id: &str) -> Vec<OutputFile> {
496        self.current_module = module_id.to_string();
497        self.collect_module_aliases(files);
498        self.collect_go_call_strategies();
499        self.collect_exported_method_names(files);
500        self.collect_impl_bounds(files);
501        self.collect_enum_layouts();
502        let mut make_functions_by_file = self.collect_make_functions();
503
504        let mut output_files = Vec::new();
505
506        let package_name = if module_id == self.ctx.entry_module {
507            "main".to_string()
508        } else {
509            let raw = module_id.rsplit('/').next().unwrap_or(module_id);
510            go_name::sanitize_package_name(raw).into_owned()
511        };
512
513        for file in files {
514            let mut source = OutputCollector::new();
515
516            if let Some(functions) = make_functions_by_file.remove(&file.id) {
517                for function in functions {
518                    source.collect_with_blank(function);
519                }
520            }
521
522            self.pending_adapter_types.clear();
523
524            for expression in &file.items {
525                self.scope.reset_for_top_level();
526                let code = self.emit_top_item(expression);
527                if !code.is_empty() {
528                    source.collect_with_blank(code);
529                }
530            }
531
532            for adapter_decl in std::mem::take(&mut self.pending_adapter_types) {
533                source.collect_with_blank(adapter_decl);
534            }
535
536            let unused_imports =
537                Self::unused_imports_for_current_module(self.ctx.unused, &self.current_module);
538            let mut import_builder = ImportBuilder::new(
539                &self.ctx.go_module,
540                unused_imports,
541                self.ctx.go_package_names,
542            );
543            import_builder.collect_from_file(file);
544
545            let ensure_imported = std::mem::take(&mut self.ensure_imported);
546            import_builder.extend_with_modules(&ensure_imported);
547
548            let flags = std::mem::take(&mut self.flags);
549            if flags.needs_fmt {
550                import_builder.require_fmt();
551            }
552            if flags.needs_stdlib {
553                import_builder.require_stdlib();
554            }
555            if flags.needs_errors {
556                import_builder.require_errors();
557            }
558            if flags.needs_slices {
559                import_builder.require_slices();
560            }
561            if flags.needs_strings {
562                import_builder.require_strings();
563            }
564            if flags.needs_maps {
565                import_builder.require_maps();
566            }
567
568            let rendered_source = source.render();
569            import_builder.filter_unreferenced(&rendered_source);
570
571            output_files.push(OutputFile {
572                name: file.go_filename(),
573                imports: import_builder.build(),
574                source: rendered_source,
575                package_name: package_name.clone(),
576            });
577        }
578
579        output_files
580    }
581}