Skip to main content

lisette_emit/
lib.rs

1mod abi;
2mod analyze;
3pub(crate) mod calls;
4pub(crate) mod context;
5pub(crate) mod control_flow;
6pub(crate) mod definitions;
7pub(crate) mod expressions;
8pub(crate) mod names;
9mod output;
10pub(crate) mod patterns;
11mod plan;
12mod render;
13mod state;
14pub(crate) mod statements;
15pub(crate) mod types;
16mod utils;
17
18pub(crate) use analyze::facts::EmitFacts;
19pub(crate) use calls::go_interop::GoCallStrategy;
20pub(crate) use context::lowering::{LineIndex, LoopContext, ReturnContext};
21pub(crate) use definitions::enum_layout::EnumLayout;
22pub(crate) use names::go_name;
23pub(crate) use names::go_name::escape_reserved;
24pub(crate) use output::OutputCollector;
25pub(crate) use render::Renderer;
26pub(crate) use state::bindings::Bindings;
27pub(crate) use state::effects::EmitEffects;
28pub(crate) use types::prelude::PreludeType;
29pub(crate) use utils::is_order_sensitive;
30pub(crate) use utils::write_line;
31
32pub use names::go_name::PRELUDE_IMPORT_PATH;
33pub use output::OutputFile;
34pub use output::imports;
35
36use rustc_hash::{FxHashMap as HashMap, FxHashSet as HashSet};
37use std::rc::Rc;
38use std::sync::Arc;
39use std::sync::OnceLock;
40
41use analyze::facts::{EmitFactsConfig, is_nullable_option};
42use names::constraints::GenericConstraintTable;
43use output::imports::ImportBuilder;
44use plan::ModulePlan;
45use plan::bodies::{LoweredBlock, LoweredStatement};
46use state::adapter_registry::AdapterRegistry;
47use state::module_state::{FunctionEmissionState, ModuleState};
48use state::scope::ScopeState;
49use syntax::ast::Span;
50use syntax::program::{
51    Definition, DefinitionBody, EmitInput, File, ModuleId, MutationInfo, UnusedInfo,
52};
53use syntax::types::{Symbol, Type};
54
55#[derive(Clone, Debug, Default)]
56pub struct EmitOptions {
57    pub sourcemap: bool,
58}
59
60#[derive(Default)]
61pub(crate) struct GlobalEmitData {
62    pub(crate) go_call_strategies: HashMap<String, GoCallStrategy>,
63    pub(crate) exported_method_names: HashSet<String>,
64    pub(crate) make_function_names: HashMap<String, String>,
65}
66
67impl GlobalEmitData {
68    fn compute(definitions: &HashMap<Symbol, Definition>) -> Self {
69        let mut globals = GlobalEmitData::default();
70
71        for prelude_type in PreludeType::enum_types() {
72            for (constructor, make_fn) in prelude_type.make_function_entries() {
73                globals.make_function_names.insert(constructor, make_fn);
74            }
75        }
76
77        for (key, definition) in definitions.iter() {
78            let is_go = go_name::is_go_import(key);
79
80            if is_go
81                && let Type::Function(f) = match definition.ty() {
82                    Type::Forall { body, .. } => body.as_ref(),
83                    other => other,
84                }
85                && let Some(strategy) =
86                    classify_go_return_type(definitions, &f.return_type, definition.go_hints())
87            {
88                globals.go_call_strategies.insert(key.to_string(), strategy);
89            }
90
91            match &definition.body {
92                DefinitionBody::Interface {
93                    definition: iface, ..
94                } if definition.visibility.is_public() => {
95                    for method_name in iface.methods.keys() {
96                        globals
97                            .exported_method_names
98                            .insert(method_name.to_string());
99                    }
100                }
101                DefinitionBody::Value { .. }
102                    if definition.visibility.is_public()
103                        && !is_go
104                        && !key.starts_with(go_name::PRELUDE_PREFIX)
105                        && key.chars().filter(|c| *c == '.').count() >= 2 =>
106                {
107                    let method_name = go_name::unqualified_name(key);
108                    globals
109                        .exported_method_names
110                        .insert(method_name.to_string());
111                }
112                _ => {}
113            }
114
115            if definition.visibility.is_public() && definition.is_display() {
116                globals
117                    .exported_method_names
118                    .insert("to_string".to_string());
119            }
120
121            if let Definition {
122                name: Some(name),
123                body: DefinitionBody::Enum { variants, .. },
124                ..
125            } = definition
126                && PreludeType::from_name(name).is_none()
127            {
128                for (constructor, make_fn) in user_enum_make_function_entries(name, variants) {
129                    globals.make_function_names.insert(constructor, make_fn);
130                }
131            }
132        }
133
134        globals
135    }
136}
137
138/// Make-function name registry entries for a user-declared enum.
139pub(crate) fn user_enum_make_function_entries<'a>(
140    name: &'a str,
141    variants: &'a [syntax::ast::EnumVariant],
142) -> impl Iterator<Item = (String, String)> + 'a {
143    let go_type_name = go_name::escape_keyword(name).into_owned();
144    variants.iter().map(move |variant| {
145        let constructor = format!("{}.{}", name, variant.name);
146        let make_fn = format!("Make{}{}", go_type_name, variant.name);
147        (constructor, make_fn)
148    })
149}
150
151pub(crate) fn classify_go_return_type(
152    definitions: &HashMap<Symbol, Definition>,
153    return_ty: &Type,
154    go_hints: &[String],
155) -> Option<GoCallStrategy> {
156    if return_ty.is_partial() {
157        return Some(GoCallStrategy::Partial);
158    }
159    if return_ty.is_result() {
160        return Some(GoCallStrategy::Result);
161    }
162    if return_ty.is_option() {
163        if let Some(value) = sentinel_hint(go_hints) {
164            return Some(GoCallStrategy::Sentinel { value });
165        }
166        if !is_nullable_option(definitions, return_ty) {
167            return Some(GoCallStrategy::CommaOk);
168        }
169        if go_hints.iter().any(|s| s == "comma_ok") {
170            return Some(GoCallStrategy::CommaOk);
171        }
172        return Some(GoCallStrategy::NullableReturn);
173    }
174    if let Some(arity) = return_ty.tuple_arity()
175        && arity >= 2
176    {
177        return Some(GoCallStrategy::Tuple { arity });
178    }
179    None
180}
181
182pub(crate) fn sentinel_hint(hints: &[String]) -> Option<i64> {
183    hints
184        .iter()
185        .any(|h| h == "sentinel_minus_one")
186        .then_some(-1)
187}
188
189pub struct TestEmitConfig<'a> {
190    pub definitions: &'a HashMap<Symbol, Definition>,
191    pub module_id: &'a str,
192    pub go_module: &'a str,
193    pub unused: &'a UnusedInfo,
194    pub mutations: &'a MutationInfo,
195    pub ufcs_methods: &'a HashSet<(String, String)>,
196    pub go_package_names: &'a HashMap<String, String>,
197    pub go_module_ids: &'a HashSet<String>,
198}
199
200pub struct Planner<'a> {
201    pub(crate) facts: EmitFacts<'a>,
202    pub(crate) module: ModuleState,
203    pub(crate) function_state: FunctionEmissionState,
204    pub(crate) scope: ScopeState,
205    pub(crate) adapter_registry: AdapterRegistry,
206}
207
208impl<'a> Planner<'a> {
209    pub(crate) fn return_context_for_type(&self, return_ty: Type) -> ReturnContext {
210        match self.classify_direct_emission(&return_ty) {
211            Some(shape) => ReturnContext::Lowered { return_ty, shape },
212            None => ReturnContext::Tagged(return_ty),
213        }
214    }
215
216    /// Append this file's newly-synthesized adapter declarations to `source`.
217    pub(crate) fn drain_file_emission_into(&mut self, source: &mut OutputCollector) {
218        for adapter_declaration in self.adapter_registry.flush_new_declarations() {
219            source.collect_with_blank(adapter_declaration);
220        }
221    }
222}
223
224impl<'a> Planner<'a> {
225    pub fn emit(analysis: &'a EmitInput, go_module: &str, options: EmitOptions) -> Vec<OutputFile> {
226        let line_indexes: Arc<HashMap<u32, LineIndex>> = Arc::new(if options.sourcemap {
227            analysis
228                .files
229                .iter()
230                .map(|(file_id, file)| {
231                    (
232                        *file_id,
233                        LineIndex::from_source(file.display_path.clone(), &file.source),
234                    )
235                })
236                .collect()
237        } else {
238            HashMap::default()
239        });
240
241        let shared = SharedEmitContext {
242            options,
243            line_indexes,
244            globals: Arc::new(GlobalEmitData::compute(&analysis.definitions)),
245            generic_base: Arc::new(OnceLock::new()),
246        };
247
248        let mut work: Vec<(&ModuleId, &syntax::program::ModuleInfo)> = analysis
249            .modules
250            .iter()
251            .filter(|(id, _)| !analysis.cached_modules.contains(*id))
252            .collect();
253        work.sort_unstable_by(|a, b| a.0.cmp(b.0));
254
255        const PARALLEL_THRESHOLD: usize = 4;
256
257        let emit_one = |&(module_id, module_info): &(&ModuleId, &syntax::program::ModuleInfo)| {
258            emit_module(analysis, go_module, &shared, module_id, module_info)
259        };
260
261        let mut output: Vec<OutputFile> = if work.len() < PARALLEL_THRESHOLD {
262            work.iter().flat_map(emit_one).collect()
263        } else {
264            use rayon::prelude::*;
265            work.par_iter().flat_map_iter(emit_one).collect()
266        };
267
268        output.sort_by(|a, b| a.name.cmp(&b.name));
269        output
270    }
271
272    pub fn new_for_tests(config: &TestEmitConfig<'a>, source: Option<&str>) -> Self {
273        let (sourcemap, line_indexes) = match source {
274            Some(src) => (
275                true,
276                Arc::new(HashMap::from_iter([(
277                    0u32,
278                    LineIndex::from_source("src/test.lis".to_string(), src),
279                )])),
280            ),
281            None => (false, Arc::new(HashMap::default())),
282        };
283        let globals = Arc::new(GlobalEmitData::compute(config.definitions));
284        let facts = EmitFacts::new(EmitFactsConfig {
285            definitions: config.definitions,
286            unused: config.unused,
287            mutations: config.mutations,
288            ufcs_methods: config.ufcs_methods,
289            go_package_names: config.go_package_names,
290            go_module_ids: config.go_module_ids,
291            entry_module: config.module_id.to_string(),
292            go_module: config.go_module.to_string(),
293            options: EmitOptions { sourcemap },
294            line_indexes,
295            globals,
296            generic_base: Arc::new(OnceLock::new()),
297            current_module: config.module_id.to_string(),
298        });
299        Self::new(facts)
300    }
301
302    fn new(facts: EmitFacts<'a>) -> Self {
303        Self {
304            facts,
305            module: ModuleState::default(),
306            function_state: FunctionEmissionState::default(),
307            scope: ScopeState::new(),
308            adapter_registry: AdapterRegistry::default(),
309        }
310    }
311
312    pub(crate) fn push_loop(&mut self, result_var: impl Into<String>) {
313        self.scope.push_loop(LoopContext {
314            result_var: result_var.into(),
315            label: None,
316        });
317    }
318
319    pub(crate) fn pop_loop(&mut self) {
320        self.scope.pop_loop();
321    }
322
323    pub(crate) fn current_loop_result_var(&self) -> Option<&str> {
324        self.scope.current_loop_result_var()
325    }
326
327    pub(crate) fn current_loop_label(&self) -> Option<&str> {
328        self.scope.current_loop_label()
329    }
330
331    /// Push the enclosing function/lambda/try/recover return context. This
332    /// scope stack is the single source of truth for return-context lowering;
333    /// all readers consult it via [`Planner::return_ctx`].
334    pub(crate) fn push_return_ctx(&mut self, ctx: ReturnContext) {
335        self.scope.push_return_ctx(Rc::new(ctx));
336    }
337
338    pub(crate) fn pop_return_ctx(&mut self) {
339        self.scope.pop_return_ctx();
340    }
341
342    /// The enclosing function/lambda/try/recover return context, maintained on
343    /// the scope stack and shared cheaply via `Rc`. Defaults to
344    /// `ReturnContext::None` outside any function body (e.g. module-level
345    /// collection). This is the single source of truth for return-context
346    /// lowering.
347    pub(crate) fn return_ctx(&self) -> Rc<ReturnContext> {
348        self.scope
349            .current_return_ctx()
350            .unwrap_or_else(|| Rc::new(ReturnContext::None))
351    }
352
353    /// `true` if this is a new declaration in the current block (use `:=`),
354    /// `false` if the name is already declared (use `=`).
355    pub(crate) fn try_declare(&mut self, go_name: &str) -> bool {
356        self.scope.try_declare_go_name(go_name)
357    }
358
359    pub(crate) fn is_declared(&self, go_name: &str) -> bool {
360        self.scope.is_go_name_declared(go_name)
361    }
362
363    /// Unconditionally marks `go_name` as declared in the current block.
364    pub(crate) fn declare(&mut self, go_name: &str) {
365        self.scope.declare_go_name(go_name);
366    }
367
368    /// Allocate a fresh Go temp, register it as declared, and emit
369    /// `tmp := value` into `output`.
370    pub(crate) fn hoist_tmp_value(
371        &mut self,
372        output: &mut String,
373        hint: &str,
374        value: &str,
375    ) -> String {
376        let tmp = self.fresh_var(Some(hint));
377        self.declare(&tmp);
378        write_line!(output, "{} := {}", tmp, value);
379        tmp
380    }
381
382    /// Structured counterpart of `hoist_tmp_value`: push a `TempBind` leaf.
383    pub(crate) fn hoist_tmp_value_statement(
384        &mut self,
385        setup: &mut Vec<LoweredStatement>,
386        hint: &str,
387        value: &str,
388    ) -> String {
389        let tmp = self.fresh_var(Some(hint));
390        self.declare(&tmp);
391        setup.push(LoweredStatement::TempBind {
392            name: tmp.clone(),
393            value: value.to_string(),
394        });
395        tmp
396    }
397
398    /// Run `f` inside a fresh scope to build a `LoweredBlock`, returning `None`
399    /// when it renders empty.
400    pub(crate) fn capture_scoped_block<F>(&mut self, f: F) -> Option<LoweredBlock>
401    where
402        F: FnOnce(&mut Self) -> LoweredBlock,
403    {
404        self.enter_scope();
405        let block = f(self);
406        self.exit_scope();
407        let mut buffer = String::new();
408        Renderer.render_lowered_block(&mut buffer, &block);
409        (!buffer.is_empty()).then_some(block)
410    }
411
412    pub(crate) fn enter_scope(&mut self) {
413        self.scope.enter_block();
414    }
415
416    pub(crate) fn exit_scope(&mut self) {
417        self.scope.exit_block();
418    }
419
420    pub(crate) fn fresh_var(&mut self, hint: Option<&str>) -> String {
421        self.scope.fresh_go_name(hint)
422    }
423
424    pub(crate) fn set_current_loop_label_if_needed(&mut self, needs_label: bool) {
425        if needs_label {
426            let label = self.fresh_var(Some("loop"));
427            self.scope.set_current_loop_label(label);
428        }
429    }
430
431    pub(crate) fn push_const_frame(&mut self) {
432        self.scope.push_const_frame();
433    }
434
435    pub(crate) fn pop_const_frame(&mut self) {
436        self.scope.pop_const_frame();
437    }
438
439    pub(crate) fn record_go_const(&mut self, go_identifier: String) {
440        self.scope.record_go_const_binding(go_identifier);
441    }
442
443    pub(crate) fn is_go_const_binding(&self, go_identifier: &str) -> bool {
444        self.scope.is_go_const_binding(go_identifier)
445    }
446
447    pub(crate) fn maybe_line_directive(&self, span: &Span) -> String {
448        if !self.facts.sourcemap_enabled() || span.is_dummy() {
449            return String::new();
450        }
451
452        let Some(source) = self.facts.line_index(span.file_id) else {
453            return String::new();
454        };
455
456        let line = source.line_for_offset(span.byte_offset);
457        let col = source.col_for_offset(span.byte_offset);
458
459        format!("//line {}:{}:{}\n", source.path, line, col)
460    }
461
462    pub fn emit_files(&mut self, files: &[&File], module_id: &str) -> Vec<OutputFile> {
463        let plan = self.build_module_plan(files, module_id);
464        self.render_module_plan(files, &plan)
465    }
466
467    fn render_module_plan(&mut self, files: &[&File], plan: &ModulePlan) -> Vec<OutputFile> {
468        let mut output_files = Vec::new();
469
470        for (i, (file, file_plan)) in files.iter().zip(&plan.files).enumerate() {
471            debug_assert_eq!(file.id, file_plan.file_id, "plan/file order mismatch");
472
473            let mut source = OutputCollector::new();
474
475            for function in &file_plan.make_functions {
476                source.collect_with_blank(function.clone());
477            }
478
479            let mut fx = EmitEffects::default();
480            for expression in &file.items {
481                self.scope.reset_for_top_level();
482                let code = self.emit_top_item(expression, &mut fx);
483                if !code.is_empty() {
484                    source.collect_with_blank(code);
485                }
486            }
487
488            let mut import_builder =
489                ImportBuilder::from_plan(&file_plan.imports, self.facts.go_package_names());
490
491            self.drain_file_emission_into(&mut source);
492            fx.drain_into(&mut import_builder);
493            if i == 0 {
494                plan.collection_effects.drain_into(&mut import_builder);
495            }
496
497            import_builder.filter_unused_imports();
498
499            let rendered_source = source.render();
500
501            let (imports, mut diagnostics) = import_builder.build();
502            if i == 0 {
503                diagnostics.extend(plan.collision_diagnostics.iter().cloned());
504            }
505            output_files.push(OutputFile {
506                name: file_plan.output_name.clone(),
507                imports,
508                source: rendered_source,
509                package_name: plan.package_name.clone(),
510                diagnostics,
511            });
512        }
513
514        output_files
515    }
516}
517
518/// Emit state built once in [`Planner::emit`] and shared (by `&`) across every
519/// module worker: options plus the three computed-once `Arc`s.
520struct SharedEmitContext {
521    options: EmitOptions,
522    line_indexes: Arc<HashMap<u32, LineIndex>>,
523    globals: Arc<GlobalEmitData>,
524    generic_base: Arc<OnceLock<GenericConstraintTable>>,
525}
526
527fn emit_module<'a>(
528    analysis: &'a EmitInput,
529    go_module: &str,
530    shared_emit_ctx: &SharedEmitContext,
531    module_id: &str,
532    module_info: &syntax::program::ModuleInfo,
533) -> Vec<OutputFile> {
534    let facts = EmitFacts::new(EmitFactsConfig {
535        definitions: &analysis.definitions,
536        unused: &analysis.unused,
537        mutations: &analysis.mutations,
538        ufcs_methods: &analysis.ufcs_methods,
539        go_package_names: &analysis.go_package_names,
540        go_module_ids: &analysis.go_module_ids,
541        entry_module: analysis.entry_module_id.to_string(),
542        go_module: go_module.to_string(),
543        options: shared_emit_ctx.options.clone(),
544        line_indexes: shared_emit_ctx.line_indexes.clone(),
545        globals: shared_emit_ctx.globals.clone(),
546        generic_base: shared_emit_ctx.generic_base.clone(),
547        current_module: module_id.to_string(),
548    });
549    let mut planner: Planner<'a> = Planner::new(facts);
550
551    let files: Vec<_> = module_info
552        .file_ids
553        .iter()
554        .filter_map(|fid| analysis.files.get(fid))
555        .collect();
556
557    let mut module_output = planner.emit_files(&files, module_id);
558
559    if module_id != analysis.entry_module_id.as_str() {
560        for file in &mut module_output {
561            file.name = format!("{}/{}", module_info.path, file.name);
562        }
563    }
564
565    module_output
566}