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
138pub(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 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 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 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 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 pub(crate) fn declare(&mut self, go_name: &str) {
365 self.scope.declare_go_name(go_name);
366 }
367
368 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 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 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
518struct 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}