Skip to main content

bock_codegen/
go.rs

1//! Go code generator — rule-based (Tier 2) transpilation from AIR to Go.
2//!
3//! Handles all capability gaps:
4//! - Records → structs
5//! - Traits → interfaces
6//! - Algebraic types → structs with tag field + type switch
7//! - Pattern matching → switch/type-switch/if-else chains
8//! - Effects → interface parameters
9//! - Ownership → erased (Go is GC)
10//! - Generics → Go type parameters (Go 1.18+)
11//! - Concurrency → goroutines/channels
12//! - Error handling → `(value, error)` return tuples
13//! - String interpolation → `fmt.Sprintf`
14
15use std::collections::{HashMap, HashSet};
16use std::fmt::Write;
17use std::path::PathBuf;
18
19use bock_air::{AIRNode, AirInterpolationPart, EnumVariantPayload, NodeKind, ResultVariant};
20use bock_ast::{AssignOp, BinOp, ImportItems, Literal, TypeExpr, UnaryOp, Visibility};
21use bock_types::AIRModule;
22
23use crate::error::CodegenError;
24use crate::generator::{CodeGenerator, GeneratedCode, OutputFile, SourceMap};
25use crate::profile::TargetProfile;
26
27/// Conservative module scan for `Channel` / `spawn` references.
28fn go_module_uses_concurrency(items: &[AIRNode]) -> bool {
29    items.iter().any(|n| {
30        let s = format!("{n:?}");
31        s.contains("\"Channel\"") || s.contains("\"spawn\"")
32    })
33}
34
35/// Runtime helpers for Bock concurrency in Go. A Channel is a wrapper
36/// over `chan interface{}` so the generic shape is simple; `spawn`
37/// launches a goroutine whose result is piped through a 1-element
38/// buffered channel (matching the existing Go async-fn wrapper
39/// convention — cf. F.4.3).
40const CONCURRENCY_RUNTIME_GO: &str = "\
41// ── Bock concurrency runtime ──
42type __bockChannel struct {
43\tq chan interface{}
44}
45
46func __bockChannelNew() (*__bockChannel, *__bockChannel) {
47\tc := &__bockChannel{q: make(chan interface{}, 1024)}
48\treturn c, c
49}
50func (c *__bockChannel) send(v interface{}) { c.q <- v }
51func (c *__bockChannel) recv() interface{}  { return <-c.q }
52func (c *__bockChannel) close()              {}
53
54// __bockSpawn launches the passed channel-returning async computation.
55// In practice the Go async-fn lowerer already wraps bodies in goroutines,
56// so this is the identity on a receive channel.
57func __bockSpawn(ch interface{}) interface{} { return ch }
58";
59
60/// Go code generator implementing the `CodeGenerator` trait.
61#[derive(Debug)]
62pub struct GoGenerator {
63    profile: TargetProfile,
64}
65
66impl GoGenerator {
67    /// Creates a new Go code generator.
68    #[must_use]
69    pub fn new() -> Self {
70        Self {
71            profile: TargetProfile::go(),
72        }
73    }
74}
75
76impl Default for GoGenerator {
77    fn default() -> Self {
78        Self::new()
79    }
80}
81
82impl CodeGenerator for GoGenerator {
83    fn target(&self) -> &TargetProfile {
84        &self.profile
85    }
86
87    fn generate_module(&self, module: &AIRModule) -> Result<GeneratedCode, CodegenError> {
88        let mut ctx = GoEmitCtx::new();
89        ctx.collect_async_fns(module);
90        ctx.emit_node(module)?;
91        let content = ctx.finish();
92        let source_map = SourceMap {
93            generated_file: "output.go".to_string(),
94            ..Default::default()
95        };
96        Ok(GeneratedCode {
97            files: vec![OutputFile {
98                path: PathBuf::from("output.go"),
99                content,
100            }],
101            source_map: Some(source_map),
102        })
103    }
104
105    fn generate_project(&self, modules: &[&AIRModule]) -> Result<GeneratedCode, CodegenError> {
106        let mut combined_body = String::new();
107        let mut needs_fmt = false;
108        let mut needs_sync = false;
109        let mut needs_time = false;
110
111        // Pre-scan async fns across all modules so cross-module calls
112        // between async functions route through the Async-suffix wrappers.
113        let mut global_async_fns: HashSet<String> = HashSet::new();
114        for module in modules {
115            if let NodeKind::Module { items, .. } = &module.kind {
116                for item in items {
117                    if let NodeKind::FnDecl {
118                        is_async: true,
119                        name,
120                        ..
121                    } = &item.kind
122                    {
123                        global_async_fns.insert(name.name.clone());
124                    }
125                }
126            }
127        }
128        for module in modules {
129            let mut ctx = GoEmitCtx::new();
130            ctx.async_fns = global_async_fns.clone();
131            ctx.emit_node(module)?;
132            let (body, fmt, sync, time) = ctx.into_parts();
133            needs_fmt |= fmt;
134            needs_sync |= sync;
135            needs_time |= time;
136            if !combined_body.is_empty() && !body.is_empty() {
137                combined_body.push('\n');
138            }
139            combined_body.push_str(&body);
140        }
141
142        // Build single preamble with merged imports
143        let mut header = "package main\n".to_string();
144        let mut imports = Vec::new();
145        if needs_fmt {
146            imports.push("\"fmt\"");
147        }
148        if needs_sync {
149            imports.push("\"sync\"");
150        }
151        if needs_time {
152            imports.push("\"time\"");
153        }
154        if !imports.is_empty() {
155            if imports.len() == 1 {
156                header.push_str(&format!("\nimport {}\n", imports[0]));
157            } else {
158                header.push_str("\nimport (\n");
159                for imp in &imports {
160                    header.push_str(&format!("\t{imp}\n"));
161                }
162                header.push_str(")\n");
163            }
164        }
165        header.push('\n');
166        header.push_str(&combined_body);
167
168        let source_map = SourceMap {
169            generated_file: "output.go".to_string(),
170            ..Default::default()
171        };
172        Ok(GeneratedCode {
173            files: vec![OutputFile {
174                path: PathBuf::from("output.go"),
175                content: header,
176            }],
177            source_map: Some(source_map),
178        })
179    }
180}
181
182// ─── Emission context ────────────────────────────────────────────────────────
183
184/// Internal state for Go emission.
185struct GoEmitCtx {
186    buf: String,
187    indent: usize,
188    /// Track whether we need `"fmt"` import.
189    needs_fmt_import: bool,
190    /// Track whether we need `"sync"` import.
191    needs_sync_import: bool,
192    /// Track whether we need `"time"` import.
193    needs_time_import: bool,
194    /// Package name (defaults to "main").
195    package_name: String,
196    /// Maps effect operation name → effect type name (e.g., "log" → "Logger").
197    effect_ops: HashMap<String, String>,
198    /// Maps effect type name → current handler variable name in scope.
199    current_handler_vars: HashMap<String, String>,
200    /// Maps function name → effect type names from its `with` clause.
201    fn_effects: HashMap<String, Vec<String>>,
202    /// Maps composite effect name → component effect names.
203    composite_effects: HashMap<String, Vec<String>>,
204    /// Names of public (exported) functions — emitted as PascalCase at call sites.
205    public_fns: HashSet<String>,
206    /// Names of effect operations that return Void — emitted without a `return` prefix.
207    void_effect_ops: HashSet<String>,
208    /// Bock names of top-level async functions. Call-site identifiers in this
209    /// set are rewritten to `fnNameAsync` so callers receive the channel form
210    /// of the function (goroutine started, `<-chan T` returned). Without this,
211    /// `await task()` would try to receive from a `T`, not `chan T`.
212    async_fns: HashSet<String>,
213}
214
215impl GoEmitCtx {
216    fn new() -> Self {
217        Self {
218            buf: String::with_capacity(4096),
219            indent: 0,
220            needs_fmt_import: false,
221            needs_sync_import: false,
222            needs_time_import: false,
223            package_name: "main".into(),
224            effect_ops: HashMap::new(),
225            current_handler_vars: HashMap::new(),
226            fn_effects: HashMap::new(),
227            composite_effects: HashMap::new(),
228            public_fns: HashSet::new(),
229            void_effect_ops: HashSet::new(),
230            async_fns: HashSet::new(),
231        }
232    }
233
234    /// Pre-scan the module for top-level `async fn` names. Must be populated
235    /// before any Call node is emitted so the Async-suffix rewrite at call
236    /// sites covers both forward and backward references within the module.
237    fn collect_async_fns(&mut self, module: &AIRNode) {
238        if let NodeKind::Module { items, .. } = &module.kind {
239            for item in items {
240                if let NodeKind::FnDecl {
241                    is_async: true,
242                    name,
243                    ..
244                } = &item.kind
245                {
246                    self.async_fns.insert(name.name.clone());
247                }
248            }
249        }
250    }
251
252    /// Returns `true` if the AIR type node represents `Void` or `Unit`.
253    fn is_void_type(node: &AIRNode) -> bool {
254        if let NodeKind::TypeNamed { path, .. } = &node.kind {
255            if let Some(last) = path.segments.last() {
256                return last.name == "Void" || last.name == "Unit";
257            }
258        }
259        if let NodeKind::TypeTuple { elems } = &node.kind {
260            return elems.is_empty();
261        }
262        false
263    }
264
265    /// Returns the emitted body and import flags without building the preamble.
266    fn into_parts(self) -> (String, bool, bool, bool) {
267        (
268            self.buf,
269            self.needs_fmt_import,
270            self.needs_sync_import,
271            self.needs_time_import,
272        )
273    }
274
275    fn finish(self) -> String {
276        let mut header = format!("package {}\n", self.package_name);
277        let mut imports = Vec::new();
278        if self.needs_fmt_import {
279            imports.push("\"fmt\"");
280        }
281        if self.needs_sync_import {
282            imports.push("\"sync\"");
283        }
284        if self.needs_time_import {
285            imports.push("\"time\"");
286        }
287        if !imports.is_empty() {
288            if imports.len() == 1 {
289                header.push_str(&format!("\nimport {}\n", imports[0]));
290            } else {
291                header.push_str("\nimport (\n");
292                for imp in &imports {
293                    header.push_str(&format!("\t{imp}\n"));
294                }
295                header.push_str(")\n");
296            }
297        }
298        header.push('\n');
299        header.push_str(&self.buf);
300        header
301    }
302
303    fn indent_str(&self) -> String {
304        "\t".repeat(self.indent)
305    }
306
307    fn write_indent(&mut self) {
308        let indent = self.indent_str();
309        self.buf.push_str(&indent);
310    }
311
312    fn writeln(&mut self, s: &str) {
313        self.write_indent();
314        self.buf.push_str(s);
315        self.buf.push('\n');
316    }
317
318    // ── Prelude function mapping ──────────────────────────────────────────
319
320    /// Emit an expression into a temporary buffer and return the string.
321    fn expr_to_string(&mut self, node: &AIRNode) -> Result<String, CodegenError> {
322        let start = self.buf.len();
323        self.emit_expr(node)?;
324        let s = self.buf[start..].to_string();
325        self.buf.truncate(start);
326        Ok(s)
327    }
328
329    /// Map Bock prelude functions to Go equivalents.
330    fn map_prelude_call(
331        &mut self,
332        callee: &AIRNode,
333        args: &[bock_air::AirArg],
334    ) -> Result<Option<String>, CodegenError> {
335        let name = match &callee.kind {
336            NodeKind::Identifier { name } => name.name.as_str(),
337            _ => return Ok(None),
338        };
339        let arg_strs: Vec<String> = args
340            .iter()
341            .map(|a| self.expr_to_string(&a.value))
342            .collect::<Result<_, _>>()?;
343        let code = match name {
344            "println" => {
345                self.needs_fmt_import = true;
346                let a = arg_strs.first().map_or(String::new(), |s| s.clone());
347                format!("fmt.Println({a})")
348            }
349            "print" => {
350                self.needs_fmt_import = true;
351                let a = arg_strs.first().map_or(String::new(), |s| s.clone());
352                format!("fmt.Print({a})")
353            }
354            "debug" => {
355                self.needs_fmt_import = true;
356                let a = arg_strs.first().map_or(String::new(), |s| s.clone());
357                format!("fmt.Printf(\"%+v\\n\", {a})")
358            }
359            "assert" => {
360                let a = arg_strs.first().map_or(String::new(), |s| s.clone());
361                format!("if !{a} {{ panic(\"assertion failed\") }}")
362            }
363            "todo" => "panic(\"not implemented\")".to_string(),
364            "unreachable" => "panic(\"unreachable\")".to_string(),
365            "sleep" => {
366                // sleep(d) returns a chan struct{} so `await` (= `<-ch`) works
367                // uniformly. The goroutine holds for `d` nanos, then closes ch.
368                self.needs_time_import = true;
369                let a = arg_strs.first().map_or(String::new(), |s| s.clone());
370                format!("(func() <-chan struct{{}} {{ __ch := make(chan struct{{}}); go func() {{ time.Sleep(time.Duration({a})); close(__ch) }}(); return __ch }})()")
371            }
372            _ => return Ok(None),
373        };
374        Ok(Some(code))
375    }
376
377    /// Recognise `Duration.xxx(...)` / `Instant.xxx(...)` associated-function
378    /// calls and emit inline Go code. Duration values are `int64` nanoseconds
379    /// (matching `time.Duration`); Instants are `time.Time` (monotonic via
380    /// `time.Now()`).
381    fn try_emit_time_assoc_call(
382        &mut self,
383        callee: &AIRNode,
384        args: &[bock_air::AirArg],
385    ) -> Result<bool, CodegenError> {
386        let NodeKind::FieldAccess { object, field } = &callee.kind else {
387            return Ok(false);
388        };
389        let NodeKind::Identifier { name: type_name } = &object.kind else {
390            return Ok(false);
391        };
392        let arg_strs: Vec<String> = args
393            .iter()
394            .map(|a| self.expr_to_string(&a.value))
395            .collect::<Result<_, _>>()?;
396        let arg0 = || arg_strs.first().cloned().unwrap_or_default();
397        let code = match (type_name.name.as_str(), field.name.as_str()) {
398            ("Duration", "zero") => "int64(0)".to_string(),
399            ("Duration", "nanos") => format!("int64({})", arg0()),
400            ("Duration", "micros") => format!("(int64({}) * 1000)", arg0()),
401            ("Duration", "millis") => format!("(int64({}) * 1000000)", arg0()),
402            ("Duration", "seconds") => format!("(int64({}) * 1000000000)", arg0()),
403            ("Duration", "minutes") => format!("(int64({}) * 60000000000)", arg0()),
404            ("Duration", "hours") => format!("(int64({}) * 3600000000000)", arg0()),
405            ("Instant", "now") => {
406                self.needs_time_import = true;
407                "time.Now()".to_string()
408            }
409            _ => return Ok(false),
410        };
411        self.buf.push_str(&code);
412        Ok(true)
413    }
414
415    /// Recognise desugared method calls on Duration/Instant values.
416    fn try_emit_time_desugared_method(
417        &mut self,
418        callee: &AIRNode,
419        args: &[bock_air::AirArg],
420    ) -> Result<bool, CodegenError> {
421        let NodeKind::FieldAccess { object, field } = &callee.kind else {
422            return Ok(false);
423        };
424        if let NodeKind::Identifier { name } = &object.kind {
425            if matches!(name.name.as_str(), "Duration" | "Instant") {
426                return Ok(false);
427            }
428        }
429        if !is_time_method_name(&field.name) {
430            return Ok(false);
431        }
432        let remaining: Vec<bock_air::AirArg> = args.iter().skip(1).cloned().collect();
433        self.try_emit_time_method(object, &field.name, &remaining)
434    }
435
436    /// Recognise `Channel.new()`, `spawn(...)`, and method calls on a
437    /// channel value. Emits calls into the Go runtime helper code
438    /// (injected at top-of-module).
439    fn try_emit_concurrency_call(
440        &mut self,
441        callee: &AIRNode,
442        args: &[bock_air::AirArg],
443    ) -> Result<bool, CodegenError> {
444        if let NodeKind::Identifier { name } = &callee.kind {
445            if name.name == "spawn" {
446                self.buf.push_str("__bockSpawn(");
447                for (i, arg) in args.iter().enumerate() {
448                    if i > 0 {
449                        self.buf.push_str(", ");
450                    }
451                    self.emit_expr(&arg.value)?;
452                }
453                self.buf.push(')');
454                return Ok(true);
455            }
456        }
457        let NodeKind::FieldAccess { object, field } = &callee.kind else {
458            return Ok(false);
459        };
460        if let NodeKind::Identifier { name: type_name } = &object.kind {
461            if type_name.name == "Channel" && field.name == "new" {
462                self.buf.push_str("__bockChannelNew()");
463                return Ok(true);
464            }
465        }
466        if matches!(field.name.as_str(), "send" | "recv" | "close") {
467            self.emit_expr(object)?;
468            let _ = write!(self.buf, ".{}", field.name);
469            self.buf.push('(');
470            for (i, arg) in args.iter().skip(1).enumerate() {
471                if i > 0 {
472                    self.buf.push_str(", ");
473                }
474                self.emit_expr(&arg.value)?;
475            }
476            self.buf.push(')');
477            return Ok(true);
478        }
479        Ok(false)
480    }
481
482    /// Recognise instance methods on Duration/Instant values.
483    fn try_emit_time_method(
484        &mut self,
485        receiver: &AIRNode,
486        method: &str,
487        args: &[bock_air::AirArg],
488    ) -> Result<bool, CodegenError> {
489        let recv_str = self.expr_to_string(receiver)?;
490        let arg_strs: Vec<String> = args
491            .iter()
492            .map(|a| self.expr_to_string(&a.value))
493            .collect::<Result<_, _>>()?;
494        let code = match method {
495            "as_nanos" => format!("({recv_str})"),
496            "as_millis" => format!("(({recv_str}) / 1000000)"),
497            "as_seconds" => format!("(({recv_str}) / 1000000000)"),
498            "is_zero" => format!("(({recv_str}) == 0)"),
499            "is_negative" => format!("(({recv_str}) < 0)"),
500            "abs" => {
501                format!("(func(__d int64) int64 {{ if __d < 0 {{ return -__d }}; return __d }}({recv_str}))")
502            }
503            "elapsed" => {
504                self.needs_time_import = true;
505                format!("int64(time.Since({recv_str}))")
506            }
507            "duration_since" => {
508                let other = arg_strs.first().cloned().unwrap_or_default();
509                format!("int64(({recv_str}).Sub({other}))")
510            }
511            _ => return Ok(false),
512        };
513        self.buf.push_str(&code);
514        Ok(true)
515    }
516
517    // ── Top-level dispatch ──────────────────────────────────────────────────
518
519    fn emit_node(&mut self, node: &AIRNode) -> Result<(), CodegenError> {
520        match &node.kind {
521            NodeKind::Module { imports, items, .. } => {
522                if go_module_uses_concurrency(items) {
523                    self.buf.push_str(CONCURRENCY_RUNTIME_GO);
524                    self.buf.push('\n');
525                }
526                for imp in imports {
527                    self.emit_node(imp)?;
528                }
529                if !imports.is_empty() && !items.is_empty() {
530                    self.buf.push('\n');
531                }
532                for (i, item) in items.iter().enumerate() {
533                    if i > 0 {
534                        self.buf.push('\n');
535                    }
536                    self.emit_node(item)?;
537                }
538                Ok(())
539            }
540            NodeKind::ImportDecl { path, items } => {
541                let path_str = path
542                    .segments
543                    .iter()
544                    .map(|s| s.name.as_str())
545                    .collect::<Vec<_>>()
546                    .join("/");
547                match items {
548                    ImportItems::Module => {
549                        self.writeln(&format!("import \"{path_str}\""));
550                    }
551                    ImportItems::Named(names) => {
552                        // Go imports are module-level; named imports are expressed as qualified access.
553                        let _ = names;
554                        self.writeln(&format!("import \"{path_str}\""));
555                    }
556                    ImportItems::Glob => {
557                        self.writeln(&format!("import . \"{path_str}\""));
558                    }
559                }
560                Ok(())
561            }
562            NodeKind::FnDecl {
563                visibility,
564                is_async,
565                name,
566                generic_params,
567                params,
568                return_type,
569                effect_clause,
570                body,
571                ..
572            } => self.emit_fn_decl(
573                *visibility,
574                *is_async,
575                &name.name,
576                generic_params,
577                params,
578                return_type.as_deref(),
579                effect_clause,
580                body,
581            ),
582            NodeKind::RecordDecl {
583                name,
584                generic_params,
585                fields,
586                ..
587            } => {
588                let type_params = self.format_generic_params(generic_params);
589                self.writeln(&format!("type {}{type_params} struct {{", name.name));
590                self.indent += 1;
591                for f in fields {
592                    let type_str = self.ast_type_to_go(&f.ty);
593                    self.writeln(&format!("{}\t{type_str}", to_pascal_case(&f.name.name)));
594                }
595                self.indent -= 1;
596                self.writeln("}");
597                Ok(())
598            }
599            NodeKind::EnumDecl {
600                name,
601                generic_params,
602                variants,
603                ..
604            } => {
605                // Go doesn't have algebraic types; use interface + variant structs.
606                let type_params = self.format_generic_params(generic_params);
607                // Emit the interface (sealed by convention).
608                self.writeln(&format!("type {}{type_params} interface {{", name.name));
609                self.indent += 1;
610                self.writeln(&format!("is{}()", name.name));
611                self.indent -= 1;
612                self.writeln("}");
613                // Emit each variant as a struct implementing the interface.
614                for variant in variants {
615                    self.buf.push('\n');
616                    self.emit_enum_variant(&name.name, generic_params, variant)?;
617                }
618                Ok(())
619            }
620            NodeKind::ClassDecl {
621                name,
622                generic_params,
623                fields,
624                methods,
625                ..
626            } => {
627                // Emit struct.
628                let type_params = self.format_generic_params(generic_params);
629                self.writeln(&format!("type {}{type_params} struct {{", name.name));
630                self.indent += 1;
631                for f in fields {
632                    let type_str = self.ast_type_to_go(&f.ty);
633                    self.writeln(&format!("{}\t{type_str}", to_pascal_case(&f.name.name)));
634                }
635                self.indent -= 1;
636                self.writeln("}");
637                // Constructor function.
638                if !fields.is_empty() {
639                    self.buf.push('\n');
640                    let params: Vec<String> = fields
641                        .iter()
642                        .map(|f| {
643                            let fname = to_camel_case(&f.name.name);
644                            let type_str = self.ast_type_to_go(&f.ty);
645                            format!("{fname} {type_str}")
646                        })
647                        .collect();
648                    self.writeln(&format!(
649                        "func New{}({}) *{} {{",
650                        name.name,
651                        params.join(", "),
652                        name.name
653                    ));
654                    self.indent += 1;
655                    let field_inits: Vec<String> = fields
656                        .iter()
657                        .map(|f| {
658                            format!(
659                                "{}: {},",
660                                to_pascal_case(&f.name.name),
661                                to_camel_case(&f.name.name)
662                            )
663                        })
664                        .collect();
665                    self.writeln(&format!("return &{} {{", name.name));
666                    self.indent += 1;
667                    for init in &field_inits {
668                        self.writeln(init);
669                    }
670                    self.indent -= 1;
671                    self.writeln("}");
672                    self.indent -= 1;
673                    self.writeln("}");
674                }
675                // Methods.
676                for method in methods {
677                    self.buf.push('\n');
678                    self.emit_method(&name.name, method, false)?;
679                }
680                Ok(())
681            }
682            NodeKind::TraitDecl { name, methods, .. } => {
683                // Traits → Go interfaces.
684                self.writeln(&format!("type {} interface {{", name.name));
685                self.indent += 1;
686                for method in methods {
687                    if let NodeKind::FnDecl {
688                        name,
689                        params,
690                        return_type,
691                        ..
692                    } = &method.kind
693                    {
694                        let param_strs = self.collect_param_type_strs(params);
695                        let is_void = return_type.as_deref().is_some_and(Self::is_void_type);
696                        let ret = if is_void {
697                            String::new()
698                        } else {
699                            return_type
700                                .as_deref()
701                                .map(|t| format!(" {}", self.type_to_go(t)))
702                                .unwrap_or_default()
703                        };
704                        self.writeln(&format!(
705                            "{}({}){ret}",
706                            to_pascal_case(&name.name),
707                            param_strs.join(", "),
708                        ));
709                    }
710                }
711                self.indent -= 1;
712                self.writeln("}");
713                Ok(())
714            }
715            NodeKind::ImplBlock {
716                target,
717                methods,
718                trait_path,
719                ..
720            } => {
721                let target_name = self.type_expr_to_string(target);
722                // Value receivers for trait/effect impls so `Handler{}` satisfies
723                // the interface; pointer receivers for inherent `impl T { ... }`.
724                let use_value_receiver = trait_path.is_some();
725                for (i, method) in methods.iter().enumerate() {
726                    if i > 0 {
727                        self.buf.push('\n');
728                    }
729                    self.emit_method(&target_name, method, use_value_receiver)?;
730                }
731                Ok(())
732            }
733            NodeKind::EffectDecl {
734                name,
735                components,
736                generic_params,
737                operations,
738                ..
739            } => {
740                if !components.is_empty() {
741                    let comp_names: Vec<String> = components
742                        .iter()
743                        .map(|tp| {
744                            tp.segments
745                                .last()
746                                .map_or("effect".to_string(), |s| s.name.clone())
747                        })
748                        .collect();
749                    self.writeln(&format!(
750                        "// composite effect {} = {}",
751                        name.name,
752                        comp_names.join(" + ")
753                    ));
754                    self.composite_effects
755                        .insert(name.name.clone(), comp_names);
756                    return Ok(());
757                }
758                // Record effect operations for Call → handler.op rewriting.
759                for op in operations {
760                    if let NodeKind::FnDecl {
761                        name: op_name,
762                        return_type,
763                        ..
764                    } = &op.kind
765                    {
766                        self.effect_ops
767                            .insert(op_name.name.clone(), name.name.clone());
768                        if return_type.as_deref().is_some_and(Self::is_void_type) {
769                            self.void_effect_ops.insert(op_name.name.clone());
770                        }
771                    }
772                }
773                // Effects → Go interfaces.
774                let type_params = self.format_generic_params(generic_params);
775                self.writeln(&format!("type {}{type_params} interface {{", name.name));
776                self.indent += 1;
777                for op in operations {
778                    if let NodeKind::FnDecl {
779                        name,
780                        params,
781                        return_type,
782                        ..
783                    } = &op.kind
784                    {
785                        let param_strs = self.collect_param_type_strs(params);
786                        let is_void = return_type.as_deref().is_some_and(Self::is_void_type);
787                        let ret = if is_void {
788                            String::new()
789                        } else {
790                            return_type
791                                .as_deref()
792                                .map(|t| format!(" {}", self.type_to_go(t)))
793                                .unwrap_or_default()
794                        };
795                        self.writeln(&format!(
796                            "{}({}){ret}",
797                            to_pascal_case(&name.name),
798                            param_strs.join(", "),
799                        ));
800                    }
801                }
802                self.indent -= 1;
803                self.writeln("}");
804                Ok(())
805            }
806            NodeKind::TypeAlias {
807                name,
808                generic_params,
809                ..
810            } => {
811                let type_params = self.format_generic_params(generic_params);
812                self.writeln(&format!("type {}{type_params} = interface{{}}", name.name));
813                Ok(())
814            }
815            NodeKind::ConstDecl {
816                name, value, ty, ..
817            } => {
818                let type_str = format!(" {}", self.type_to_go(ty));
819                let ind = self.indent_str();
820                let _ = write!(
821                    self.buf,
822                    "{ind}var {}{type_str} = ",
823                    to_pascal_case(&name.name)
824                );
825                self.emit_expr(value)?;
826                self.buf.push('\n');
827                Ok(())
828            }
829            NodeKind::ModuleHandle { effect, handler } => {
830                let effect_name =
831                    effect.segments.last().map_or("effect", |s| s.name.as_str());
832                let var_name = format!("__{}", to_camel_case(effect_name));
833                let ind = self.indent_str();
834                let _ = write!(self.buf, "{ind}var {var_name} {effect_name} = ");
835                self.emit_expr(handler)?;
836                self.buf.push('\n');
837                // Register the module-scoped handler so effectful function
838                // calls at module level pick it up.
839                self.current_handler_vars
840                    .insert(effect_name.to_string(), var_name);
841                Ok(())
842            }
843            NodeKind::PropertyTest { name, .. } => {
844                self.writeln(&format!("// property test: {name}"));
845                Ok(())
846            }
847            // Statement / expression nodes at top level:
848            NodeKind::LetBinding { .. }
849            | NodeKind::If { .. }
850            | NodeKind::For { .. }
851            | NodeKind::While { .. }
852            | NodeKind::Loop { .. }
853            | NodeKind::Return { .. }
854            | NodeKind::Break { .. }
855            | NodeKind::Continue
856            | NodeKind::Guard { .. }
857            | NodeKind::Match { .. }
858            | NodeKind::Block { .. }
859            | NodeKind::HandlingBlock { .. }
860            | NodeKind::Assign { .. } => self.emit_stmt(node),
861            _ => {
862                self.write_indent();
863                self.emit_expr(node)?;
864                self.buf.push('\n');
865                Ok(())
866            }
867        }
868    }
869
870    // ── Generics ────────────────────────────────────────────────────────────
871
872    fn format_generic_params(&self, params: &[bock_ast::GenericParam]) -> String {
873        if params.is_empty() {
874            return String::new();
875        }
876        let parts: Vec<String> = params
877            .iter()
878            .map(|p| {
879                if p.bounds.is_empty() {
880                    format!("{} any", p.name.name)
881                } else {
882                    let bound_strs: Vec<String> = p
883                        .bounds
884                        .iter()
885                        .map(|b| {
886                            b.segments
887                                .iter()
888                                .map(|s| s.name.as_str())
889                                .collect::<Vec<_>>()
890                                .join(".")
891                        })
892                        .collect();
893                    format!("{} {}", p.name.name, bound_strs.join(" | "))
894                }
895            })
896            .collect();
897        format!("[{}]", parts.join(", "))
898    }
899
900    fn format_generic_args(&self, args: &[AIRNode]) -> String {
901        if args.is_empty() {
902            return String::new();
903        }
904        let parts: Vec<String> = args.iter().map(|a| self.type_to_go(a)).collect();
905        format!("[{}]", parts.join(", "))
906    }
907
908    // ── Function declarations ───────────────────────────────────────────────
909
910    #[allow(clippy::too_many_arguments)]
911    fn emit_fn_decl(
912        &mut self,
913        visibility: Visibility,
914        is_async: bool,
915        name: &str,
916        generic_params: &[bock_ast::GenericParam],
917        params: &[AIRNode],
918        return_type: Option<&AIRNode>,
919        effect_clause: &[bock_ast::TypePath],
920        body: &AIRNode,
921    ) -> Result<(), CodegenError> {
922        let is_public = matches!(visibility, Visibility::Public);
923        let fn_name = if is_public {
924            to_pascal_case(name)
925        } else {
926            to_camel_case(name)
927        };
928        if is_public {
929            self.public_fns.insert(name.to_string());
930        }
931        let type_params = self.format_generic_params(generic_params);
932        let param_strs = self.collect_param_strs(params);
933        let effects = self.effects_params(effect_clause);
934        let mut all_params = param_strs.clone();
935        all_params.extend(effects.clone());
936        let is_void = return_type.is_some_and(Self::is_void_type);
937        let ret = if is_void {
938            String::new()
939        } else {
940            return_type
941                .map(|t| format!(" {}", self.type_to_go(t)))
942                .unwrap_or_default()
943        };
944        if !effect_clause.is_empty() {
945            let effect_names = self.expand_effect_names(effect_clause);
946            self.fn_effects.insert(name.to_string(), effect_names);
947        }
948        self.writeln(&format!(
949            "func {fn_name}{type_params}({}){ret} {{",
950            all_params.join(", "),
951        ));
952        self.indent += 1;
953        let old_handler_vars = self.current_handler_vars.clone();
954        let expanded = self.expand_effect_names(effect_clause);
955        for ename in &expanded {
956            self.current_handler_vars
957                .insert(ename.clone(), to_camel_case(ename));
958        }
959        if name == "main" || is_void {
960            self.emit_block_body(body)?;
961        } else {
962            self.emit_block_body_return(body)?;
963        }
964        self.current_handler_vars = old_handler_vars;
965        self.indent -= 1;
966        self.writeln("}");
967
968        // Async wrapper: every `async fn` gets a companion `FnAsync` that
969        // starts a goroutine and returns a buffered `<-chan T` (or
970        // `<-chan struct{}` for void returns). `main` is skipped — Go's
971        // entry point is always `func main()` and wrapping it would be dead
972        // code the linker would complain about.
973        if is_async && name != "main" {
974            self.buf.push('\n');
975            self.emit_async_wrapper(
976                &fn_name,
977                &type_params,
978                params,
979                return_type,
980                is_void,
981                &effects,
982            )?;
983        }
984        Ok(())
985    }
986
987    /// Emit the `FnNameAsync` companion for an `async fn`. The wrapper starts
988    /// a goroutine, invokes the sync body with the caller's arguments, and
989    /// returns the result over a buffered channel. Callers `await`
990    /// (= `<-chan T`) to observe completion.
991    fn emit_async_wrapper(
992        &mut self,
993        sync_fn_name: &str,
994        type_params: &str,
995        params: &[AIRNode],
996        return_type: Option<&AIRNode>,
997        is_void: bool,
998        effects: &[String],
999    ) -> Result<(), CodegenError> {
1000        let async_fn_name = format!("{sync_fn_name}Async");
1001        let param_strs = self.collect_param_strs(params);
1002        let mut all_params = param_strs;
1003        all_params.extend(effects.iter().cloned());
1004        let chan_ty = if is_void {
1005            "struct{}".to_string()
1006        } else {
1007            return_type
1008                .map(|t| self.type_to_go(t))
1009                .unwrap_or_else(|| "interface{}".to_string())
1010        };
1011        self.writeln(&format!(
1012            "func {async_fn_name}{type_params}({}) <-chan {chan_ty} {{",
1013            all_params.join(", "),
1014        ));
1015        self.indent += 1;
1016        self.writeln(&format!("__ch := make(chan {chan_ty}, 1)"));
1017        self.writeln("go func() {");
1018        self.indent += 1;
1019        // Forward the sync function's arguments verbatim. Param names are
1020        // the camel-cased binding names the wrapper receives.
1021        let call_args: Vec<String> = params
1022            .iter()
1023            .filter_map(|p| {
1024                if let NodeKind::Param { pattern, .. } = &p.kind {
1025                    Some(to_camel_case(&self.pattern_to_binding_name(pattern)))
1026                } else {
1027                    None
1028                }
1029            })
1030            .chain(effects.iter().map(|e| {
1031                // Effects params look like `name EffectType`; recover the
1032                // name before the first space.
1033                e.split_whitespace()
1034                    .next()
1035                    .unwrap_or("")
1036                    .to_string()
1037            }))
1038            .collect();
1039        let call_site = format!("{sync_fn_name}({})", call_args.join(", "));
1040        if is_void {
1041            self.writeln(&call_site);
1042            self.writeln("__ch <- struct{}{}");
1043        } else {
1044            self.writeln(&format!("__ch <- {call_site}"));
1045        }
1046        self.indent -= 1;
1047        self.writeln("}()");
1048        self.writeln("return __ch");
1049        self.indent -= 1;
1050        self.writeln("}");
1051        Ok(())
1052    }
1053
1054    fn emit_method(
1055        &mut self,
1056        receiver_type: &str,
1057        method: &AIRNode,
1058        use_value_receiver: bool,
1059    ) -> Result<(), CodegenError> {
1060        if let NodeKind::FnDecl {
1061            visibility,
1062            name,
1063            params,
1064            return_type,
1065            effect_clause,
1066            body,
1067            ..
1068        } = &method.kind
1069        {
1070            let method_name = if matches!(visibility, Visibility::Public) {
1071                to_pascal_case(&name.name)
1072            } else {
1073                to_camel_case(&name.name)
1074            };
1075            let receiver_var = receiver_type
1076                .chars()
1077                .next()
1078                .unwrap_or('r')
1079                .to_lowercase()
1080                .to_string();
1081            let param_strs = self.collect_param_strs(params);
1082            let effects = self.effects_params(effect_clause);
1083            let mut all_params = param_strs;
1084            all_params.extend(effects);
1085            let is_void = return_type.as_deref().is_some_and(Self::is_void_type);
1086            let ret = if is_void {
1087                String::new()
1088            } else {
1089                return_type
1090                    .as_deref()
1091                    .map(|t| format!(" {}", self.type_to_go(t)))
1092                    .unwrap_or_default()
1093            };
1094            let receiver_prefix = if use_value_receiver { "" } else { "*" };
1095            self.writeln(&format!(
1096                "func ({receiver_var} {receiver_prefix}{receiver_type}) {method_name}({}){ret} {{",
1097                all_params.join(", "),
1098            ));
1099            self.indent += 1;
1100            let old_handler_vars = self.current_handler_vars.clone();
1101            let expanded = self.expand_effect_names(effect_clause);
1102            for ename in &expanded {
1103                self.current_handler_vars
1104                    .insert(ename.clone(), to_camel_case(ename));
1105            }
1106            if return_type.is_some() && !is_void {
1107                self.emit_block_body_return(body)?;
1108            } else {
1109                self.emit_block_body(body)?;
1110            }
1111            self.current_handler_vars = old_handler_vars;
1112            self.indent -= 1;
1113            self.writeln("}");
1114        }
1115        Ok(())
1116    }
1117
1118    fn collect_param_strs(&self, params: &[AIRNode]) -> Vec<String> {
1119        params
1120            .iter()
1121            .filter_map(|p| {
1122                if let NodeKind::Param { pattern, ty, .. } = &p.kind {
1123                    let name = to_camel_case(&self.pattern_to_binding_name(pattern));
1124                    let type_str = ty
1125                        .as_ref()
1126                        .map(|t| format!(" {}", self.type_to_go(t)))
1127                        .unwrap_or_else(|| " interface{}".into());
1128                    Some(format!("{name}{type_str}"))
1129                } else {
1130                    None
1131                }
1132            })
1133            .collect()
1134    }
1135
1136    fn collect_param_type_strs(&self, params: &[AIRNode]) -> Vec<String> {
1137        params
1138            .iter()
1139            .filter_map(|p| {
1140                if let NodeKind::Param { ty, .. } = &p.kind {
1141                    let type_str = ty
1142                        .as_ref()
1143                        .map(|t| self.type_to_go(t))
1144                        .unwrap_or_else(|| "interface{}".into());
1145                    Some(type_str)
1146                } else {
1147                    None
1148                }
1149            })
1150            .collect()
1151    }
1152
1153    /// Expand effect names, replacing composite effects with their components.
1154    fn expand_effect_names(&self, effects: &[bock_ast::TypePath]) -> Vec<String> {
1155        let mut result = Vec::new();
1156        for tp in effects {
1157            let name = tp
1158                .segments
1159                .last()
1160                .map_or("effect".to_string(), |s| s.name.clone());
1161            if let Some(components) = self.composite_effects.get(&name) {
1162                result.extend(components.iter().cloned());
1163            } else {
1164                result.push(name);
1165            }
1166        }
1167        result
1168    }
1169
1170    /// Effects → interface parameters: `log Log, clock Clock`.
1171    fn effects_params(&self, effects: &[bock_ast::TypePath]) -> Vec<String> {
1172        let expanded = self.expand_effect_names(effects);
1173        expanded
1174            .iter()
1175            .map(|name| format!("{} {}", to_camel_case(name), name))
1176            .collect()
1177    }
1178
1179    /// Build `handler_var, ...` arguments for calling an effectful function.
1180    fn build_effects_call_args_go(&self, fn_name: &str) -> Option<String> {
1181        let effects = self.fn_effects.get(fn_name)?;
1182        let entries: Vec<String> = effects
1183            .iter()
1184            .filter_map(|e| {
1185                let handler_var = self.current_handler_vars.get(e)?;
1186                Some(handler_var.clone())
1187            })
1188            .collect();
1189        if entries.is_empty() {
1190            return None;
1191        }
1192        Some(entries.join(", "))
1193    }
1194
1195    // ── Enum variant structs ────────────────────────────────────────────────
1196
1197    fn emit_enum_variant(
1198        &mut self,
1199        enum_name: &str,
1200        generic_params: &[bock_ast::GenericParam],
1201        variant: &AIRNode,
1202    ) -> Result<(), CodegenError> {
1203        if let NodeKind::EnumVariant { name, payload } = &variant.kind {
1204            let vname = &name.name;
1205            let type_params = self.format_generic_params(generic_params);
1206            match payload {
1207                EnumVariantPayload::Unit => {
1208                    self.writeln(&format!("type {enum_name}{vname}{type_params} struct{{}}"));
1209                }
1210                EnumVariantPayload::Struct(fields) => {
1211                    self.writeln(&format!("type {enum_name}{vname}{type_params} struct {{"));
1212                    self.indent += 1;
1213                    for f in fields {
1214                        let type_str = self.ast_type_to_go(&f.ty);
1215                        self.writeln(&format!("{}\t{type_str}", to_pascal_case(&f.name.name)));
1216                    }
1217                    self.indent -= 1;
1218                    self.writeln("}");
1219                }
1220                EnumVariantPayload::Tuple(elems) => {
1221                    self.writeln(&format!("type {enum_name}{vname}{type_params} struct {{"));
1222                    self.indent += 1;
1223                    for (i, elem) in elems.iter().enumerate() {
1224                        let type_str = self.type_to_go(elem);
1225                        self.writeln(&format!("Field{i}\t{type_str}"));
1226                    }
1227                    self.indent -= 1;
1228                    self.writeln("}");
1229                }
1230            }
1231            // Implement the interface marker method.
1232            self.buf.push('\n');
1233            self.writeln(&format!(
1234                "func ({enum_name}{vname}{type_params}) is{enum_name}() {{}}"
1235            ));
1236        }
1237        Ok(())
1238    }
1239
1240    // ── Statements ──────────────────────────────────────────────────────────
1241
1242    fn emit_stmt(&mut self, node: &AIRNode) -> Result<(), CodegenError> {
1243        match &node.kind {
1244            NodeKind::LetBinding {
1245                pattern, value, ty, ..
1246            } => {
1247                let binding = self.pattern_to_go_binding(pattern);
1248                if let Some(t) = ty {
1249                    let type_str = self.type_to_go(t);
1250                    let ind = self.indent_str();
1251                    let _ = write!(self.buf, "{ind}var {binding} {type_str} = ");
1252                    self.emit_expr(value)?;
1253                    self.buf.push('\n');
1254                } else {
1255                    let ind = self.indent_str();
1256                    let _ = write!(self.buf, "{ind}{binding} := ");
1257                    self.emit_expr(value)?;
1258                    self.buf.push('\n');
1259                }
1260                Ok(())
1261            }
1262            NodeKind::If {
1263                let_pattern,
1264                condition,
1265                then_block,
1266                else_block,
1267            } => {
1268                if let Some(pat) = let_pattern {
1269                    let binding = self.pattern_to_go_binding(pat);
1270                    let ind = self.indent_str();
1271                    let _ = write!(self.buf, "{ind}{binding} := ");
1272                    self.emit_expr(condition)?;
1273                    self.buf.push('\n');
1274                    self.writeln(&format!("if {binding} != nil {{"));
1275                    self.indent += 1;
1276                    self.emit_block_body(then_block)?;
1277                    self.indent -= 1;
1278                } else {
1279                    let ind = self.indent_str();
1280                    let _ = write!(self.buf, "{ind}if ");
1281                    self.emit_expr(condition)?;
1282                    self.buf.push_str(" {\n");
1283                    self.indent += 1;
1284                    self.emit_block_body(then_block)?;
1285                    self.indent -= 1;
1286                }
1287                if let Some(else_b) = else_block {
1288                    if matches!(else_b.kind, NodeKind::If { .. }) {
1289                        let ind = self.indent_str();
1290                        let _ = write!(self.buf, "{ind}}} else ");
1291                        // Emit the if without leading indent.
1292                        self.emit_if_continued(else_b)?;
1293                        return Ok(());
1294                    }
1295                    self.writeln("} else {");
1296                    self.indent += 1;
1297                    self.emit_block_body(else_b)?;
1298                    self.indent -= 1;
1299                }
1300                self.writeln("}");
1301                Ok(())
1302            }
1303            NodeKind::For {
1304                pattern,
1305                iterable,
1306                body,
1307            } => {
1308                let binding = self.pattern_to_go_binding(pattern);
1309                let ind = self.indent_str();
1310                let _ = write!(self.buf, "{ind}for _, {binding} := range ");
1311                self.emit_expr(iterable)?;
1312                self.buf.push_str(" {\n");
1313                self.indent += 1;
1314                self.emit_block_body(body)?;
1315                self.indent -= 1;
1316                self.writeln("}");
1317                Ok(())
1318            }
1319            NodeKind::While { condition, body } => {
1320                let ind = self.indent_str();
1321                let _ = write!(self.buf, "{ind}for ");
1322                self.emit_expr(condition)?;
1323                self.buf.push_str(" {\n");
1324                self.indent += 1;
1325                self.emit_block_body(body)?;
1326                self.indent -= 1;
1327                self.writeln("}");
1328                Ok(())
1329            }
1330            NodeKind::Loop { body } => {
1331                self.writeln("for {");
1332                self.indent += 1;
1333                self.emit_block_body(body)?;
1334                self.indent -= 1;
1335                self.writeln("}");
1336                Ok(())
1337            }
1338            NodeKind::Return { value } => {
1339                if let Some(val) = value {
1340                    let ind = self.indent_str();
1341                    let _ = write!(self.buf, "{ind}return ");
1342                    self.emit_expr(val)?;
1343                    self.buf.push('\n');
1344                } else {
1345                    self.writeln("return");
1346                }
1347                Ok(())
1348            }
1349            NodeKind::Break { value } => {
1350                if let Some(val) = value {
1351                    let ind = self.indent_str();
1352                    let _ = write!(self.buf, "{ind}// break value: ");
1353                    self.emit_expr(val)?;
1354                    self.buf.push('\n');
1355                    self.writeln("break");
1356                } else {
1357                    self.writeln("break");
1358                }
1359                Ok(())
1360            }
1361            NodeKind::Continue => {
1362                self.writeln("continue");
1363                Ok(())
1364            }
1365            NodeKind::Guard {
1366                condition,
1367                else_block,
1368                ..
1369            } => {
1370                let ind = self.indent_str();
1371                let _ = write!(self.buf, "{ind}if !(");
1372                self.emit_expr(condition)?;
1373                self.buf.push_str(") {\n");
1374                self.indent += 1;
1375                self.emit_block_body(else_block)?;
1376                self.indent -= 1;
1377                self.writeln("}");
1378                Ok(())
1379            }
1380            NodeKind::Match { scrutinee, arms } => self.emit_match(scrutinee, arms),
1381            NodeKind::Block { stmts, tail } => {
1382                for s in stmts {
1383                    self.emit_node(s)?;
1384                }
1385                if let Some(t) = tail {
1386                    self.write_indent();
1387                    self.emit_expr(t)?;
1388                    self.buf.push('\n');
1389                }
1390                Ok(())
1391            }
1392            NodeKind::HandlingBlock { handlers, body } => {
1393                // handling block → scoped handler instantiation
1394                self.writeln("{");
1395                self.indent += 1;
1396                let old_handler_vars = self.current_handler_vars.clone();
1397                let mut new_var_names = Vec::with_capacity(handlers.len());
1398                for h in handlers {
1399                    let effect_name =
1400                        h.effect.segments.last().map_or("effect", |s| s.name.as_str());
1401                    let var_name = format!("__{}", to_camel_case(effect_name));
1402                    let ind = self.indent_str();
1403                    let _ = write!(self.buf, "{ind}{var_name} := ");
1404                    self.emit_expr(&h.handler)?;
1405                    self.buf.push('\n');
1406                    self.current_handler_vars
1407                        .insert(effect_name.to_string(), var_name.clone());
1408                    new_var_names.push(var_name);
1409                }
1410                // Suppress Go's "declared but not used" error when a handler
1411                // is declared in an outer handling scope and only referenced
1412                // indirectly through inner handling blocks (or not at all).
1413                for v in &new_var_names {
1414                    self.writeln(&format!("_ = {v}"));
1415                }
1416                if let NodeKind::Block { stmts, tail } = &body.kind {
1417                    for s in stmts {
1418                        self.emit_node(s)?;
1419                    }
1420                    if let Some(t) = tail {
1421                        self.write_indent();
1422                        self.emit_expr(t)?;
1423                        self.buf.push('\n');
1424                    }
1425                } else {
1426                    self.emit_stmt(body)?;
1427                }
1428                self.current_handler_vars = old_handler_vars;
1429                self.indent -= 1;
1430                self.writeln("}");
1431                Ok(())
1432            }
1433            NodeKind::Assign { op, target, value } => {
1434                let ind = self.indent_str();
1435                let _ = write!(self.buf, "{ind}");
1436                self.emit_expr(target)?;
1437                let op_str = match op {
1438                    AssignOp::Assign => " = ",
1439                    AssignOp::AddAssign => " += ",
1440                    AssignOp::SubAssign => " -= ",
1441                    AssignOp::MulAssign => " *= ",
1442                    AssignOp::DivAssign => " /= ",
1443                    AssignOp::RemAssign => " %= ",
1444                };
1445                self.buf.push_str(op_str);
1446                self.emit_expr(value)?;
1447                self.buf.push('\n');
1448                Ok(())
1449            }
1450            _ => {
1451                self.write_indent();
1452                self.emit_expr(node)?;
1453                self.buf.push('\n');
1454                Ok(())
1455            }
1456        }
1457    }
1458
1459    /// Emit an if statement that continues after an `} else`.
1460    fn emit_if_continued(&mut self, node: &AIRNode) -> Result<(), CodegenError> {
1461        if let NodeKind::If {
1462            condition,
1463            then_block,
1464            else_block,
1465            ..
1466        } = &node.kind
1467        {
1468            let _ = write!(self.buf, "if ");
1469            self.emit_expr(condition)?;
1470            self.buf.push_str(" {\n");
1471            self.indent += 1;
1472            self.emit_block_body(then_block)?;
1473            self.indent -= 1;
1474            if let Some(else_b) = else_block {
1475                if matches!(else_b.kind, NodeKind::If { .. }) {
1476                    let ind = self.indent_str();
1477                    let _ = write!(self.buf, "{ind}}} else ");
1478                    return self.emit_if_continued(else_b);
1479                }
1480                self.writeln("} else {");
1481                self.indent += 1;
1482                self.emit_block_body(else_b)?;
1483                self.indent -= 1;
1484            }
1485            self.writeln("}");
1486        }
1487        Ok(())
1488    }
1489
1490    // ── Expressions ─────────────────────────────────────────────────────────
1491
1492    fn emit_expr(&mut self, node: &AIRNode) -> Result<(), CodegenError> {
1493        match &node.kind {
1494            NodeKind::Literal { lit } => {
1495                match lit {
1496                    Literal::Int(s) => self.buf.push_str(s),
1497                    Literal::Float(s) => self.buf.push_str(s),
1498                    Literal::Bool(b) => self.buf.push_str(if *b { "true" } else { "false" }),
1499                    Literal::Char(s) => {
1500                        self.buf.push('\'');
1501                        self.buf.push_str(s);
1502                        self.buf.push('\'');
1503                    }
1504                    Literal::String(s) => {
1505                        self.buf.push('"');
1506                        self.buf.push_str(&escape_go_string(s));
1507                        self.buf.push('"');
1508                    }
1509                    Literal::Unit => self.buf.push_str("nil"),
1510                }
1511                Ok(())
1512            }
1513            NodeKind::Identifier { name } => {
1514                let emitted = if is_prelude_ctor(&name.name) {
1515                    name.name.clone()
1516                } else if self.public_fns.contains(&name.name) {
1517                    to_pascal_case(&name.name)
1518                } else {
1519                    to_camel_case(&name.name)
1520                };
1521                self.buf.push_str(&emitted);
1522                Ok(())
1523            }
1524            NodeKind::BinaryOp { op, left, right } => {
1525                self.buf.push('(');
1526                self.emit_expr(left)?;
1527                let op_str = match op {
1528                    BinOp::Add => " + ",
1529                    BinOp::Sub => " - ",
1530                    BinOp::Mul => " * ",
1531                    BinOp::Div => " / ",
1532                    BinOp::Rem => " % ",
1533                    BinOp::Pow => " /* pow */ ",
1534                    BinOp::Eq => " == ",
1535                    BinOp::Ne => " != ",
1536                    BinOp::Lt => " < ",
1537                    BinOp::Le => " <= ",
1538                    BinOp::Gt => " > ",
1539                    BinOp::Ge => " >= ",
1540                    BinOp::And => " && ",
1541                    BinOp::Or => " || ",
1542                    BinOp::BitAnd => " & ",
1543                    BinOp::BitOr => " | ",
1544                    BinOp::BitXor => " ^ ",
1545                    BinOp::Compose => " /* compose */ ",
1546                    BinOp::Is => " == ",
1547                };
1548                self.buf.push_str(op_str);
1549                self.emit_expr(right)?;
1550                self.buf.push(')');
1551                Ok(())
1552            }
1553            NodeKind::UnaryOp { op, operand } => {
1554                let op_str = match op {
1555                    UnaryOp::Neg => "-",
1556                    UnaryOp::Not => "!",
1557                    UnaryOp::BitNot => "^",
1558                };
1559                self.buf.push_str(op_str);
1560                self.emit_expr(operand)?;
1561                Ok(())
1562            }
1563            NodeKind::Call {
1564                callee,
1565                args,
1566                type_args,
1567            } => {
1568                // Effect operation Call → handler.Op rewriting.
1569                if let NodeKind::Identifier { name } = &callee.kind {
1570                    if let Some(effect_name) = self.effect_ops.get(&name.name).cloned() {
1571                        if let Some(handler_var) =
1572                            self.current_handler_vars.get(&effect_name).cloned()
1573                        {
1574                            let _ = write!(
1575                                self.buf,
1576                                "{}.{}",
1577                                handler_var,
1578                                to_pascal_case(&name.name)
1579                            );
1580                            self.buf.push('(');
1581                            for (i, arg) in args.iter().enumerate() {
1582                                if i > 0 {
1583                                    self.buf.push_str(", ");
1584                                }
1585                                self.emit_expr(&arg.value)?;
1586                            }
1587                            self.buf.push(')');
1588                            return Ok(());
1589                        }
1590                    }
1591                }
1592                if let Some(code) = self.map_prelude_call(callee, args)? {
1593                    self.buf.push_str(&code);
1594                    return Ok(());
1595                }
1596                if self.try_emit_time_assoc_call(callee, args)? {
1597                    return Ok(());
1598                }
1599                if self.try_emit_time_desugared_method(callee, args)? {
1600                    return Ok(());
1601                }
1602                if self.try_emit_concurrency_call(callee, args)? {
1603                    return Ok(());
1604                }
1605                // Pass handler args to effectful function calls.
1606                let effects_args = if let NodeKind::Identifier { name } = &callee.kind {
1607                    self.build_effects_call_args_go(&name.name)
1608                } else {
1609                    None
1610                };
1611                // Route async-fn calls through their `Async`-suffix wrapper
1612                // so callers receive a `<-chan T` instead of `T` — the sync
1613                // body is only invoked from inside its own wrapper.
1614                if let NodeKind::Identifier { name } = &callee.kind {
1615                    if self.async_fns.contains(&name.name) {
1616                        let go_name = if self.public_fns.contains(&name.name) {
1617                            to_pascal_case(&name.name)
1618                        } else {
1619                            to_camel_case(&name.name)
1620                        };
1621                        self.buf.push_str(&format!("{go_name}Async"));
1622                    } else {
1623                        self.emit_expr(callee)?;
1624                    }
1625                } else {
1626                    self.emit_expr(callee)?;
1627                }
1628                let type_arg_str = self.format_generic_args(type_args);
1629                self.buf.push_str(&type_arg_str);
1630                self.buf.push('(');
1631                for (i, arg) in args.iter().enumerate() {
1632                    if i > 0 {
1633                        self.buf.push_str(", ");
1634                    }
1635                    self.emit_expr(&arg.value)?;
1636                }
1637                if let Some(ea) = effects_args {
1638                    if !args.is_empty() {
1639                        self.buf.push_str(", ");
1640                    }
1641                    self.buf.push_str(&ea);
1642                }
1643                self.buf.push(')');
1644                Ok(())
1645            }
1646            NodeKind::MethodCall {
1647                receiver,
1648                method,
1649                args,
1650                ..
1651            } => {
1652                if self.try_emit_time_method(receiver, &method.name, args)? {
1653                    return Ok(());
1654                }
1655                self.emit_expr(receiver)?;
1656                let _ = write!(self.buf, ".{}", to_pascal_case(&method.name));
1657                self.buf.push('(');
1658                for (i, arg) in args.iter().enumerate() {
1659                    if i > 0 {
1660                        self.buf.push_str(", ");
1661                    }
1662                    self.emit_expr(&arg.value)?;
1663                }
1664                self.buf.push(')');
1665                Ok(())
1666            }
1667            NodeKind::FieldAccess { object, field } => {
1668                self.emit_expr(object)?;
1669                let _ = write!(self.buf, ".{}", to_pascal_case(&field.name));
1670                Ok(())
1671            }
1672            NodeKind::Index { object, index } => {
1673                self.emit_expr(object)?;
1674                self.buf.push('[');
1675                self.emit_expr(index)?;
1676                self.buf.push(']');
1677                Ok(())
1678            }
1679            NodeKind::Lambda { params, body } => {
1680                let param_strs = self.collect_param_strs(params);
1681                // Determine return type from body context (best effort).
1682                let _ = write!(
1683                    self.buf,
1684                    "func({}) interface{{}} {{ return ",
1685                    param_strs.join(", ")
1686                );
1687                self.emit_expr(body)?;
1688                self.buf.push_str(" }");
1689                Ok(())
1690            }
1691            NodeKind::Pipe { left, right } => self.emit_pipe(left, right),
1692            NodeKind::Compose { left, right } => {
1693                // `f >> g` → `func(x interface{}) interface{} { return g(f(x)) }`
1694                let _ = write!(self.buf, "func(x interface{{}}) interface{{}} {{ return ");
1695                self.emit_expr(right)?;
1696                self.buf.push('(');
1697                self.emit_expr(left)?;
1698                self.buf.push_str("(x)) }");
1699                Ok(())
1700            }
1701            NodeKind::Await { expr } => {
1702                // Go uses goroutines/channels; await maps to channel receive.
1703                self.buf.push_str("<-");
1704                self.emit_expr(expr)?;
1705                Ok(())
1706            }
1707            NodeKind::Propagate { expr } => {
1708                // Go error propagation would require special handling;
1709                // just emit the expression for now.
1710                self.emit_expr(expr)?;
1711                Ok(())
1712            }
1713            NodeKind::Range { lo, hi, inclusive } => {
1714                // Go doesn't have range expressions as values;
1715                // emit as a comment-annotated slice or helper call.
1716                self.needs_fmt_import = true;
1717                self.buf.push_str("/* range */ nil");
1718                let _ = (lo, hi, inclusive);
1719                Ok(())
1720            }
1721            NodeKind::RecordConstruct {
1722                path,
1723                fields,
1724                spread,
1725            } => {
1726                let type_name = path
1727                    .segments
1728                    .iter()
1729                    .map(|s| s.name.as_str())
1730                    .collect::<Vec<_>>()
1731                    .join(".");
1732                if let Some(_sp) = spread {
1733                    // Go doesn't have spread syntax; emit TODO comment.
1734                    self.buf.push_str(&format!("{type_name}{{"));
1735                    self.buf.push_str("/* spread */ ");
1736                    for (i, f) in fields.iter().enumerate() {
1737                        if i > 0 {
1738                            self.buf.push_str(", ");
1739                        }
1740                        let _ = write!(self.buf, "{}: ", to_pascal_case(&f.name.name));
1741                        if let Some(val) = &f.value {
1742                            self.emit_expr(val)?;
1743                        } else {
1744                            self.buf.push_str(&to_camel_case(&f.name.name));
1745                        }
1746                    }
1747                    self.buf.push('}');
1748                } else {
1749                    self.buf.push_str(&format!("{type_name}{{"));
1750                    for (i, f) in fields.iter().enumerate() {
1751                        if i > 0 {
1752                            self.buf.push_str(", ");
1753                        }
1754                        let _ = write!(self.buf, "{}: ", to_pascal_case(&f.name.name));
1755                        if let Some(val) = &f.value {
1756                            self.emit_expr(val)?;
1757                        } else {
1758                            self.buf.push_str(&to_camel_case(&f.name.name));
1759                        }
1760                    }
1761                    self.buf.push('}');
1762                }
1763                Ok(())
1764            }
1765            NodeKind::ListLiteral { elems } => {
1766                self.buf.push_str("[]interface{}{");
1767                for (i, e) in elems.iter().enumerate() {
1768                    if i > 0 {
1769                        self.buf.push_str(", ");
1770                    }
1771                    self.emit_expr(e)?;
1772                }
1773                self.buf.push('}');
1774                Ok(())
1775            }
1776            NodeKind::MapLiteral { entries } => {
1777                self.buf.push_str("map[interface{}]interface{}{");
1778                for (i, entry) in entries.iter().enumerate() {
1779                    if i > 0 {
1780                        self.buf.push_str(", ");
1781                    }
1782                    self.emit_expr(&entry.key)?;
1783                    self.buf.push_str(": ");
1784                    self.emit_expr(&entry.value)?;
1785                }
1786                self.buf.push('}');
1787                Ok(())
1788            }
1789            NodeKind::SetLiteral { elems } => {
1790                // Go doesn't have sets; use map[T]struct{}.
1791                self.buf.push_str("map[interface{}]struct{}{");
1792                for (i, e) in elems.iter().enumerate() {
1793                    if i > 0 {
1794                        self.buf.push_str(", ");
1795                    }
1796                    self.emit_expr(e)?;
1797                    self.buf.push_str(": {}");
1798                }
1799                self.buf.push('}');
1800                Ok(())
1801            }
1802            NodeKind::TupleLiteral { elems } => {
1803                // Go doesn't have tuples; emit as a struct literal or slice.
1804                self.buf.push_str("[...]interface{}{");
1805                for (i, e) in elems.iter().enumerate() {
1806                    if i > 0 {
1807                        self.buf.push_str(", ");
1808                    }
1809                    self.emit_expr(e)?;
1810                }
1811                self.buf.push('}');
1812                Ok(())
1813            }
1814            NodeKind::Interpolation { parts } => {
1815                self.needs_fmt_import = true;
1816                self.buf.push_str("fmt.Sprintf(\"");
1817                let mut args = Vec::new();
1818                for part in parts {
1819                    match part {
1820                        AirInterpolationPart::Literal(s) => {
1821                            self.buf.push_str(&escape_go_string(s));
1822                        }
1823                        AirInterpolationPart::Expr(expr) => {
1824                            self.buf.push_str("%v");
1825                            args.push(expr.clone());
1826                        }
1827                    }
1828                }
1829                self.buf.push('"');
1830                for arg in &args {
1831                    self.buf.push_str(", ");
1832                    self.emit_expr(arg)?;
1833                }
1834                self.buf.push(')');
1835                Ok(())
1836            }
1837            NodeKind::Placeholder => {
1838                self.buf.push('_');
1839                Ok(())
1840            }
1841            NodeKind::Unreachable => {
1842                self.buf.push_str("panic(\"unreachable\")");
1843                Ok(())
1844            }
1845            NodeKind::ResultConstruct { variant, value } => {
1846                match variant {
1847                    ResultVariant::Ok => {
1848                        if let Some(v) = value {
1849                            self.emit_expr(v)?;
1850                            self.buf.push_str(", nil");
1851                        } else {
1852                            self.buf.push_str("nil, nil");
1853                        }
1854                    }
1855                    ResultVariant::Err => {
1856                        self.needs_fmt_import = true;
1857                        if let Some(v) = value {
1858                            self.buf.push_str("nil, ");
1859                            self.emit_expr(v)?;
1860                        } else {
1861                            self.buf.push_str("nil, fmt.Errorf(\"error\")");
1862                        }
1863                    }
1864                }
1865                Ok(())
1866            }
1867            NodeKind::Assign { op, target, value } => {
1868                self.emit_expr(target)?;
1869                let op_str = match op {
1870                    AssignOp::Assign => " = ",
1871                    AssignOp::AddAssign => " += ",
1872                    AssignOp::SubAssign => " -= ",
1873                    AssignOp::MulAssign => " *= ",
1874                    AssignOp::DivAssign => " /= ",
1875                    AssignOp::RemAssign => " %= ",
1876                };
1877                self.buf.push_str(op_str);
1878                self.emit_expr(value)?;
1879                Ok(())
1880            }
1881            NodeKind::If {
1882                condition,
1883                then_block,
1884                else_block,
1885                ..
1886            } => {
1887                // If in expression position: Go doesn't have ternary;
1888                // emit as IIFE.
1889                self.buf.push_str("func() interface{} { if ");
1890                self.emit_expr(condition)?;
1891                self.buf.push_str(" { return ");
1892                self.emit_block_as_expr(then_block)?;
1893                self.buf.push_str(" } else { return ");
1894                if let Some(eb) = else_block {
1895                    self.emit_block_as_expr(eb)?;
1896                } else {
1897                    self.buf.push_str("nil");
1898                }
1899                self.buf.push_str(" } }()");
1900                Ok(())
1901            }
1902            NodeKind::Block { stmts, tail } => {
1903                if stmts.is_empty() {
1904                    if let Some(t) = tail {
1905                        return self.emit_expr(t);
1906                    }
1907                }
1908                // Fallback: IIFE.
1909                self.buf.push_str("func() interface{} { return ");
1910                if let Some(t) = tail {
1911                    self.emit_expr(t)?;
1912                } else {
1913                    self.buf.push_str("nil");
1914                }
1915                self.buf.push_str(" }()");
1916                Ok(())
1917            }
1918            NodeKind::Match { scrutinee, arms } => {
1919                // Match in expression position: emit as IIFE with switch.
1920                self.buf.push_str("func() interface{} { switch ");
1921                self.emit_expr(scrutinee)?;
1922                self.buf.push_str(" { ");
1923                for arm in arms {
1924                    if let NodeKind::MatchArm { pattern, body, .. } = &arm.kind {
1925                        if matches!(pattern.kind, NodeKind::WildcardPat) {
1926                            self.buf.push_str("default: return ");
1927                        } else {
1928                            self.buf.push_str("case ");
1929                            self.emit_match_case_condition(pattern)?;
1930                            self.buf.push_str(": return ");
1931                        }
1932                        self.emit_block_as_expr(body)?;
1933                        self.buf.push(' ');
1934                    }
1935                }
1936                self.buf.push_str("} return nil }()");
1937                Ok(())
1938            }
1939            // Ownership nodes: erase in Go.
1940            NodeKind::Move { expr }
1941            | NodeKind::Borrow { expr }
1942            | NodeKind::MutableBorrow { expr } => self.emit_expr(expr),
1943            // Effect operation invocation.
1944            NodeKind::EffectOp {
1945                effect,
1946                operation,
1947                args,
1948            } => {
1949                let effect_name = effect.segments.last().map_or("effect", |s| s.name.as_str());
1950                let _ = write!(
1951                    self.buf,
1952                    "{}.{}",
1953                    to_camel_case(effect_name),
1954                    to_pascal_case(&operation.name)
1955                );
1956                self.buf.push('(');
1957                for (i, arg) in args.iter().enumerate() {
1958                    if i > 0 {
1959                        self.buf.push_str(", ");
1960                    }
1961                    self.emit_expr(&arg.value)?;
1962                }
1963                self.buf.push(')');
1964                Ok(())
1965            }
1966            // Type expressions: erased in Go expression context.
1967            NodeKind::TypeNamed { .. }
1968            | NodeKind::TypeTuple { .. }
1969            | NodeKind::TypeFunction { .. }
1970            | NodeKind::TypeOptional { .. }
1971            | NodeKind::TypeSelf => {
1972                self.buf.push_str("/* type */");
1973                Ok(())
1974            }
1975            NodeKind::EffectRef { path } => {
1976                let name = path
1977                    .segments
1978                    .iter()
1979                    .map(|s| s.name.as_str())
1980                    .collect::<Vec<_>>()
1981                    .join(".");
1982                self.buf.push_str(&name);
1983                Ok(())
1984            }
1985            NodeKind::Error => {
1986                self.buf.push_str("/* error */");
1987                Ok(())
1988            }
1989            _ => {
1990                self.buf.push_str("/* unsupported */");
1991                Ok(())
1992            }
1993        }
1994    }
1995
1996    // ── Match → switch/if-else ──────────────────────────────────────────────
1997
1998    fn emit_match(&mut self, scrutinee: &AIRNode, arms: &[AIRNode]) -> Result<(), CodegenError> {
1999        let ind = self.indent_str();
2000        let _ = write!(self.buf, "{ind}switch __v := ");
2001        self.emit_expr(scrutinee)?;
2002        self.buf.push_str("; __v.(type) {\n");
2003        self.indent += 1;
2004        for arm in arms {
2005            self.emit_match_arm(arm)?;
2006        }
2007        self.indent -= 1;
2008        self.writeln("}");
2009        Ok(())
2010    }
2011
2012    fn emit_match_arm(&mut self, arm: &AIRNode) -> Result<(), CodegenError> {
2013        if let NodeKind::MatchArm {
2014            pattern,
2015            guard,
2016            body,
2017        } = &arm.kind
2018        {
2019            let ind = self.indent_str();
2020            match &pattern.kind {
2021                NodeKind::WildcardPat => {
2022                    let _ = write!(self.buf, "{ind}default:");
2023                }
2024                _ => {
2025                    let _ = write!(self.buf, "{ind}case ");
2026                    self.emit_match_case_condition(pattern)?;
2027                    self.buf.push(':');
2028                }
2029            }
2030            self.buf.push('\n');
2031            self.indent += 1;
2032            if let Some(g) = guard {
2033                let gi = self.indent_str();
2034                let _ = write!(self.buf, "{gi}if ");
2035                self.emit_expr(g)?;
2036                self.buf.push_str(" {\n");
2037                self.indent += 1;
2038                self.emit_block_body(body)?;
2039                self.indent -= 1;
2040                self.writeln("}");
2041            } else {
2042                self.emit_block_body(body)?;
2043            }
2044        }
2045        Ok(())
2046    }
2047
2048    fn emit_match_case_condition(&mut self, pat: &AIRNode) -> Result<(), CodegenError> {
2049        match &pat.kind {
2050            NodeKind::WildcardPat => {
2051                self.buf.push('_');
2052            }
2053            NodeKind::BindPat { name, .. } => {
2054                let _ = name;
2055                self.buf.push_str("interface{}");
2056            }
2057            NodeKind::LiteralPat { lit } => match lit {
2058                Literal::Int(s) => self.buf.push_str(s),
2059                Literal::Float(s) => self.buf.push_str(s),
2060                Literal::Bool(b) => self.buf.push_str(if *b { "true" } else { "false" }),
2061                Literal::Char(s) => {
2062                    self.buf.push('\'');
2063                    self.buf.push_str(s);
2064                    self.buf.push('\'');
2065                }
2066                Literal::String(s) => {
2067                    self.buf.push('"');
2068                    self.buf.push_str(&escape_go_string(s));
2069                    self.buf.push('"');
2070                }
2071                Literal::Unit => self.buf.push_str("nil"),
2072            },
2073            NodeKind::ConstructorPat { path, .. } => {
2074                let variant_name = path
2075                    .segments
2076                    .iter()
2077                    .map(|s| s.name.as_str())
2078                    .collect::<Vec<_>>()
2079                    .join("");
2080                self.buf.push_str(&variant_name);
2081            }
2082            NodeKind::RecordPat { path, .. } => {
2083                let type_name = path
2084                    .segments
2085                    .iter()
2086                    .map(|s| s.name.as_str())
2087                    .collect::<Vec<_>>()
2088                    .join(".");
2089                self.buf.push_str(&type_name);
2090            }
2091            NodeKind::TuplePat { .. } => {
2092                self.buf.push_str("interface{}");
2093            }
2094            _ => {
2095                self.buf.push_str("interface{}");
2096            }
2097        }
2098        Ok(())
2099    }
2100
2101    // ── Pipe operator ───────────────────────────────────────────────────────
2102
2103    fn emit_pipe(&mut self, left: &AIRNode, right: &AIRNode) -> Result<(), CodegenError> {
2104        if let NodeKind::Call { callee, args, .. } = &right.kind {
2105            let has_placeholder = args
2106                .iter()
2107                .any(|a| matches!(a.value.kind, NodeKind::Placeholder));
2108            if has_placeholder {
2109                self.emit_expr(callee)?;
2110                self.buf.push('(');
2111                for (i, arg) in args.iter().enumerate() {
2112                    if i > 0 {
2113                        self.buf.push_str(", ");
2114                    }
2115                    if matches!(arg.value.kind, NodeKind::Placeholder) {
2116                        self.emit_expr(left)?;
2117                    } else {
2118                        self.emit_expr(&arg.value)?;
2119                    }
2120                }
2121                self.buf.push(')');
2122                return Ok(());
2123            }
2124        }
2125        self.emit_expr(right)?;
2126        self.buf.push('(');
2127        self.emit_expr(left)?;
2128        self.buf.push(')');
2129        Ok(())
2130    }
2131
2132    // ── Type emission ───────────────────────────────────────────────────────
2133
2134    fn type_to_go(&self, node: &AIRNode) -> String {
2135        match &node.kind {
2136            NodeKind::TypeNamed { path, args } => {
2137                let name = path
2138                    .segments
2139                    .iter()
2140                    .map(|s| s.name.as_str())
2141                    .collect::<Vec<_>>()
2142                    .join(".");
2143                let go_name = self.map_type_name(&name);
2144                if args.is_empty() {
2145                    go_name
2146                } else {
2147                    let arg_strs: Vec<String> = args.iter().map(|a| self.type_to_go(a)).collect();
2148                    format!("{go_name}[{}]", arg_strs.join(", "))
2149                }
2150            }
2151            NodeKind::TypeTuple { elems } => {
2152                // Go doesn't have tuples; emit as struct with numbered fields.
2153                if elems.is_empty() {
2154                    "struct{}".into()
2155                } else {
2156                    let fields: Vec<String> = elems
2157                        .iter()
2158                        .enumerate()
2159                        .map(|(i, e)| format!("Field{i} {}", self.type_to_go(e)))
2160                        .collect();
2161                    format!("struct{{ {} }}", fields.join("; "))
2162                }
2163            }
2164            NodeKind::TypeFunction { params, ret, .. } => {
2165                let param_strs: Vec<String> = params.iter().map(|p| self.type_to_go(p)).collect();
2166                format!("func({}) {}", param_strs.join(", "), self.type_to_go(ret))
2167            }
2168            NodeKind::TypeOptional { inner } => {
2169                format!("*{}", self.type_to_go(inner))
2170            }
2171            NodeKind::TypeSelf => "/* Self */".into(),
2172            _ => "interface{}".into(),
2173        }
2174    }
2175
2176    fn map_type_name(&self, name: &str) -> String {
2177        match name {
2178            "Int" => "int64".into(),
2179            "Float" => "float64".into(),
2180            "Bool" => "bool".into(),
2181            "String" => "string".into(),
2182            "Void" | "Unit" => "struct{}".into(),
2183            "List" => "[]interface{}".into(),
2184            "Map" => "map[string]interface{}".into(),
2185            "Set" => "map[interface{}]struct{}".into(),
2186            "Any" => "interface{}".into(),
2187            "Never" => "interface{}".into(),
2188            "Channel" => "*__bockChannel".into(),
2189            other => other.into(),
2190        }
2191    }
2192
2193    fn ast_type_to_go(&self, ty: &TypeExpr) -> String {
2194        match ty {
2195            TypeExpr::Named { path, args, .. } => {
2196                let name = path
2197                    .segments
2198                    .iter()
2199                    .map(|s| s.name.as_str())
2200                    .collect::<Vec<_>>()
2201                    .join(".");
2202                let go_name = self.map_type_name(&name);
2203                if args.is_empty() {
2204                    go_name
2205                } else {
2206                    let arg_strs: Vec<String> =
2207                        args.iter().map(|a| self.ast_type_to_go(a)).collect();
2208                    format!("{go_name}[{}]", arg_strs.join(", "))
2209                }
2210            }
2211            TypeExpr::Tuple { elems, .. } => {
2212                if elems.is_empty() {
2213                    "struct{}".into()
2214                } else {
2215                    let fields: Vec<String> = elems
2216                        .iter()
2217                        .enumerate()
2218                        .map(|(i, e)| format!("Field{i} {}", self.ast_type_to_go(e)))
2219                        .collect();
2220                    format!("struct{{ {} }}", fields.join("; "))
2221                }
2222            }
2223            TypeExpr::Function { params, ret, .. } => {
2224                let param_strs: Vec<String> =
2225                    params.iter().map(|p| self.ast_type_to_go(p)).collect();
2226                format!(
2227                    "func({}) {}",
2228                    param_strs.join(", "),
2229                    self.ast_type_to_go(ret)
2230                )
2231            }
2232            TypeExpr::Optional { inner, .. } => {
2233                format!("*{}", self.ast_type_to_go(inner))
2234            }
2235            TypeExpr::SelfType { .. } => "/* Self */".into(),
2236        }
2237    }
2238
2239    // ── Helpers ─────────────────────────────────────────────────────────────
2240
2241    fn emit_block_body(&mut self, node: &AIRNode) -> Result<(), CodegenError> {
2242        self.emit_block_body_inner(node, false)
2243    }
2244
2245    fn emit_block_body_return(&mut self, node: &AIRNode) -> Result<(), CodegenError> {
2246        self.emit_block_body_inner(node, true)
2247    }
2248
2249    fn emit_block_body_inner(
2250        &mut self,
2251        node: &AIRNode,
2252        emit_return: bool,
2253    ) -> Result<(), CodegenError> {
2254        if let NodeKind::Block { stmts, tail } = &node.kind {
2255            if stmts.is_empty() && tail.is_none() {
2256                self.writeln("// empty");
2257                return Ok(());
2258            }
2259            for s in stmts {
2260                self.emit_node(s)?;
2261            }
2262            if let Some(t) = tail {
2263                let should_return = emit_return && !self.is_void_call(t);
2264                if should_return {
2265                    let ind = self.indent_str();
2266                    let _ = write!(self.buf, "{ind}return ");
2267                    self.emit_expr(t)?;
2268                    self.buf.push('\n');
2269                } else {
2270                    self.write_indent();
2271                    self.emit_expr(t)?;
2272                    self.buf.push('\n');
2273                }
2274            }
2275        } else {
2276            // Single expression as body.
2277            let should_return = emit_return && !self.is_void_call(node);
2278            if should_return {
2279                let ind = self.indent_str();
2280                let _ = write!(self.buf, "{ind}return ");
2281                self.emit_expr(node)?;
2282                self.buf.push('\n');
2283            } else {
2284                self.write_indent();
2285                self.emit_expr(node)?;
2286                self.buf.push('\n');
2287            }
2288        }
2289        Ok(())
2290    }
2291
2292    /// Returns `true` if the expression is a call to a known void function
2293    /// (prelude or a Void-returning effect operation).
2294    fn is_void_call(&self, node: &AIRNode) -> bool {
2295        if let NodeKind::Call { callee, .. } = &node.kind {
2296            if let NodeKind::Identifier { name } = &callee.kind {
2297                if matches!(
2298                    name.name.as_str(),
2299                    "println" | "print" | "debug" | "assert" | "todo" | "unreachable"
2300                ) {
2301                    return true;
2302                }
2303                if self.void_effect_ops.contains(&name.name) {
2304                    return true;
2305                }
2306            }
2307        }
2308        false
2309    }
2310
2311    fn emit_block_as_expr(&mut self, node: &AIRNode) -> Result<(), CodegenError> {
2312        if let NodeKind::Block { stmts, tail } = &node.kind {
2313            if stmts.is_empty() {
2314                if let Some(t) = tail {
2315                    return self.emit_expr(t);
2316                }
2317            }
2318        }
2319        self.emit_expr(node)
2320    }
2321
2322    fn pattern_to_binding_name(&self, pat: &AIRNode) -> String {
2323        match &pat.kind {
2324            NodeKind::BindPat { name, .. } => to_camel_case(&name.name),
2325            NodeKind::WildcardPat => "_".into(),
2326            NodeKind::TuplePat { elems } => {
2327                // Go doesn't have tuple destructuring; use first element.
2328                elems
2329                    .first()
2330                    .map(|e| self.pattern_to_binding_name(e))
2331                    .unwrap_or_else(|| "_".into())
2332            }
2333            NodeKind::RecordPat { fields, .. } => fields
2334                .first()
2335                .map(|f| to_camel_case(&f.name.name))
2336                .unwrap_or_else(|| "_".into()),
2337            _ => "_".into(),
2338        }
2339    }
2340
2341    fn pattern_to_go_binding(&self, pat: &AIRNode) -> String {
2342        self.pattern_to_binding_name(pat)
2343    }
2344
2345    fn type_expr_to_string(&self, node: &AIRNode) -> String {
2346        match &node.kind {
2347            NodeKind::TypeNamed { path, .. } => path
2348                .segments
2349                .iter()
2350                .map(|s| s.name.as_str())
2351                .collect::<Vec<_>>()
2352                .join("."),
2353            NodeKind::Identifier { name } => name.name.clone(),
2354            _ => "Unknown".into(),
2355        }
2356    }
2357}
2358
2359// ─── Utility functions ───────────────────────────────────────────────────────
2360
2361/// True for Bock's built-in Optional/Result constructors, which must be
2362/// emitted verbatim (PascalCase preserved) so generated Go code can match
2363/// the runtime prelude's `Some`/`None`/`Ok`/`Err` types.
2364fn is_prelude_ctor(s: &str) -> bool {
2365    matches!(s, "Some" | "None" | "Ok" | "Err")
2366}
2367
2368/// Convert a name to `camelCase` (Go unexported).
2369fn to_camel_case(s: &str) -> String {
2370    if s.is_empty() || s == "_" {
2371        return s.to_string();
2372    }
2373    // If already camelCase (starts lowercase, no underscores), return as-is.
2374    if !s.contains('_') && s.starts_with(|c: char| c.is_lowercase()) {
2375        return s.to_string();
2376    }
2377    // If it's snake_case, convert to camelCase.
2378    if s.contains('_') {
2379        let parts: Vec<&str> = s.split('_').filter(|p| !p.is_empty()).collect();
2380        if parts.is_empty() {
2381            return s.to_string();
2382        }
2383        let mut result = parts[0].to_lowercase();
2384        for part in &parts[1..] {
2385            let mut chars = part.chars();
2386            if let Some(first) = chars.next() {
2387                result.push(
2388                    first
2389                        .to_uppercase()
2390                        .next()
2391                        .expect("uppercase yields at least one char"),
2392                );
2393                result.extend(chars);
2394            }
2395        }
2396        return result;
2397    }
2398    // If PascalCase, lowercase first letter.
2399    let mut chars = s.chars();
2400    let first = chars.next().expect("non-empty string guaranteed by caller");
2401    let mut result = first.to_lowercase().to_string();
2402    result.extend(chars);
2403    result
2404}
2405
2406/// Returns true if `name` is the identifier of a Duration or Instant instance
2407/// method. Used to recognise `d.as_millis()` / `i.elapsed()` calls during codegen.
2408fn is_time_method_name(name: &str) -> bool {
2409    matches!(
2410        name,
2411        "as_nanos"
2412            | "as_millis"
2413            | "as_seconds"
2414            | "is_zero"
2415            | "is_negative"
2416            | "abs"
2417            | "elapsed"
2418            | "duration_since"
2419    )
2420}
2421
2422/// Convert a name to `PascalCase` (Go exported).
2423fn to_pascal_case(s: &str) -> String {
2424    if s.is_empty() || s == "_" {
2425        return s.to_string();
2426    }
2427    // If it's snake_case, convert to PascalCase.
2428    if s.contains('_') {
2429        let parts: Vec<&str> = s.split('_').filter(|p| !p.is_empty()).collect();
2430        let mut result = String::new();
2431        for part in &parts {
2432            let mut chars = part.chars();
2433            if let Some(first) = chars.next() {
2434                result.push(
2435                    first
2436                        .to_uppercase()
2437                        .next()
2438                        .expect("uppercase yields at least one char"),
2439                );
2440                result.extend(chars);
2441            }
2442        }
2443        return result;
2444    }
2445    // Already PascalCase or camelCase — uppercase first letter.
2446    let mut chars = s.chars();
2447    let first = chars.next().expect("non-empty string guaranteed by caller");
2448    let mut result = first.to_uppercase().to_string();
2449    result.extend(chars);
2450    result
2451}
2452
2453/// Escape special characters in a Go string literal.
2454fn escape_go_string(s: &str) -> String {
2455    let mut out = String::with_capacity(s.len());
2456    for ch in s.chars() {
2457        match ch {
2458            '"' => out.push_str("\\\""),
2459            '\\' => out.push_str("\\\\"),
2460            '\n' => out.push_str("\\n"),
2461            '\r' => out.push_str("\\r"),
2462            '\t' => out.push_str("\\t"),
2463            _ => out.push(ch),
2464        }
2465    }
2466    out
2467}
2468
2469// ─── Tests ───────────────────────────────────────────────────────────────────
2470
2471#[cfg(test)]
2472mod tests {
2473    use super::*;
2474    use bock_air::{AirArg, AirRecordField};
2475    use bock_ast::{Ident, TypePath};
2476    use bock_errors::{FileId, Span};
2477
2478    fn span() -> Span {
2479        Span {
2480            file: FileId(0),
2481            start: 0,
2482            end: 0,
2483        }
2484    }
2485
2486    fn ident(name: &str) -> Ident {
2487        Ident {
2488            name: name.to_string(),
2489            span: span(),
2490        }
2491    }
2492
2493    fn type_path(segments: &[&str]) -> TypePath {
2494        TypePath {
2495            segments: segments.iter().map(|s| ident(s)).collect(),
2496            span: span(),
2497        }
2498    }
2499
2500    fn node(id: u32, kind: NodeKind) -> AIRNode {
2501        AIRNode::new(id, span(), kind)
2502    }
2503
2504    fn int_lit(id: u32, val: &str) -> AIRNode {
2505        node(
2506            id,
2507            NodeKind::Literal {
2508                lit: Literal::Int(val.into()),
2509            },
2510        )
2511    }
2512
2513    fn str_lit(id: u32, val: &str) -> AIRNode {
2514        node(
2515            id,
2516            NodeKind::Literal {
2517                lit: Literal::String(val.into()),
2518            },
2519        )
2520    }
2521
2522    fn bool_lit(id: u32, val: bool) -> AIRNode {
2523        node(
2524            id,
2525            NodeKind::Literal {
2526                lit: Literal::Bool(val),
2527            },
2528        )
2529    }
2530
2531    fn id_node(id: u32, name: &str) -> AIRNode {
2532        node(id, NodeKind::Identifier { name: ident(name) })
2533    }
2534
2535    fn bind_pat(id: u32, name: &str) -> AIRNode {
2536        node(
2537            id,
2538            NodeKind::BindPat {
2539                name: ident(name),
2540                is_mut: false,
2541            },
2542        )
2543    }
2544
2545    fn param_node(id: u32, name: &str) -> AIRNode {
2546        node(
2547            id,
2548            NodeKind::Param {
2549                pattern: Box::new(bind_pat(id + 100, name)),
2550                ty: None,
2551                default: None,
2552            },
2553        )
2554    }
2555
2556    fn typed_param_node(id: u32, name: &str, ty_name: &str) -> AIRNode {
2557        node(
2558            id,
2559            NodeKind::Param {
2560                pattern: Box::new(bind_pat(id + 100, name)),
2561                ty: Some(Box::new(node(
2562                    id + 200,
2563                    NodeKind::TypeNamed {
2564                        path: type_path(&[ty_name]),
2565                        args: vec![],
2566                    },
2567                ))),
2568                default: None,
2569            },
2570        )
2571    }
2572
2573    fn block(id: u32, stmts: Vec<AIRNode>, tail: Option<AIRNode>) -> AIRNode {
2574        node(
2575            id,
2576            NodeKind::Block {
2577                stmts,
2578                tail: tail.map(Box::new),
2579            },
2580        )
2581    }
2582
2583    fn module(imports: Vec<AIRNode>, items: Vec<AIRNode>) -> AIRNode {
2584        node(
2585            0,
2586            NodeKind::Module {
2587                path: None,
2588                annotations: vec![],
2589                imports,
2590                items,
2591            },
2592        )
2593    }
2594
2595    fn gen(module: &AIRNode) -> String {
2596        let gen = GoGenerator::new();
2597        let result = gen.generate_module(module).unwrap();
2598        result.files[0].content.clone()
2599    }
2600
2601    // ── Basic tests ─────────────────────────────────────────────────────────
2602
2603    #[test]
2604    fn implements_code_generator_trait() {
2605        let gen = GoGenerator::new();
2606        assert_eq!(gen.target().id, "go");
2607    }
2608
2609    #[test]
2610    fn empty_module() {
2611        let m = module(vec![], vec![]);
2612        let out = gen(&m);
2613        assert!(out.contains("package main"), "got: {out}");
2614    }
2615
2616    #[test]
2617    fn simple_function() {
2618        let body = block(2, vec![], Some(int_lit(3, "42")));
2619        let f = node(
2620            1,
2621            NodeKind::FnDecl {
2622                annotations: vec![],
2623                visibility: Visibility::Private,
2624                is_async: false,
2625                name: ident("answer"),
2626                generic_params: vec![],
2627                params: vec![],
2628                return_type: None,
2629                effect_clause: vec![],
2630                where_clause: vec![],
2631                body: Box::new(body),
2632            },
2633        );
2634        let out = gen(&module(vec![], vec![f]));
2635        assert!(out.contains("func answer()"), "got: {out}");
2636        assert!(out.contains("return 42"), "got: {out}");
2637    }
2638
2639    #[test]
2640    fn public_function_is_pascal_case() {
2641        let body = block(2, vec![], Some(int_lit(3, "42")));
2642        let f = node(
2643            1,
2644            NodeKind::FnDecl {
2645                annotations: vec![],
2646                visibility: Visibility::Public,
2647                is_async: false,
2648                name: ident("getAnswer"),
2649                generic_params: vec![],
2650                params: vec![],
2651                return_type: None,
2652                effect_clause: vec![],
2653                where_clause: vec![],
2654                body: Box::new(body),
2655            },
2656        );
2657        let out = gen(&module(vec![], vec![f]));
2658        assert!(out.contains("func GetAnswer()"), "got: {out}");
2659    }
2660
2661    #[test]
2662    fn function_with_params_and_types() {
2663        let body = block(
2664            5,
2665            vec![],
2666            Some(node(
2667                6,
2668                NodeKind::BinaryOp {
2669                    op: BinOp::Add,
2670                    left: Box::new(id_node(7, "a")),
2671                    right: Box::new(id_node(8, "b")),
2672                },
2673            )),
2674        );
2675        let f = node(
2676            1,
2677            NodeKind::FnDecl {
2678                annotations: vec![],
2679                visibility: Visibility::Public,
2680                is_async: false,
2681                name: ident("add"),
2682                generic_params: vec![],
2683                params: vec![
2684                    typed_param_node(2, "a", "Int"),
2685                    typed_param_node(3, "b", "Int"),
2686                ],
2687                return_type: Some(Box::new(node(
2688                    4,
2689                    NodeKind::TypeNamed {
2690                        path: type_path(&["Int"]),
2691                        args: vec![],
2692                    },
2693                ))),
2694                effect_clause: vec![],
2695                where_clause: vec![],
2696                body: Box::new(body),
2697            },
2698        );
2699        let out = gen(&module(vec![], vec![f]));
2700        assert!(
2701            out.contains("func Add(a int64, b int64) int64 {"),
2702            "got: {out}"
2703        );
2704        assert!(out.contains("(a + b)"), "got: {out}");
2705    }
2706
2707    #[test]
2708    fn record_to_struct() {
2709        let rec = node(
2710            1,
2711            NodeKind::RecordDecl {
2712                annotations: vec![],
2713                visibility: Visibility::Public,
2714                name: ident("Point"),
2715                generic_params: vec![],
2716                fields: vec![
2717                    bock_ast::RecordDeclField {
2718                        id: 0,
2719                        span: span(),
2720                        name: ident("x"),
2721                        ty: TypeExpr::Named {
2722                            id: 0,
2723                            span: span(),
2724                            path: type_path(&["Float"]),
2725                            args: vec![],
2726                        },
2727                        default: None,
2728                    },
2729                    bock_ast::RecordDeclField {
2730                        id: 1,
2731                        span: span(),
2732                        name: ident("y"),
2733                        ty: TypeExpr::Named {
2734                            id: 1,
2735                            span: span(),
2736                            path: type_path(&["Float"]),
2737                            args: vec![],
2738                        },
2739                        default: None,
2740                    },
2741                ],
2742            },
2743        );
2744        let out = gen(&module(vec![], vec![rec]));
2745        assert!(out.contains("type Point struct {"), "got: {out}");
2746        assert!(out.contains("X\tfloat64"), "got: {out}");
2747        assert!(out.contains("Y\tfloat64"), "got: {out}");
2748    }
2749
2750    #[test]
2751    fn trait_to_interface() {
2752        let t = node(
2753            1,
2754            NodeKind::TraitDecl {
2755                annotations: vec![],
2756                visibility: Visibility::Public,
2757                is_platform: false,
2758                name: ident("Drawable"),
2759                generic_params: vec![],
2760                associated_types: vec![],
2761                methods: vec![node(
2762                    2,
2763                    NodeKind::FnDecl {
2764                        annotations: vec![],
2765                        visibility: Visibility::Public,
2766                        is_async: false,
2767                        name: ident("draw"),
2768                        generic_params: vec![],
2769                        params: vec![],
2770                        return_type: None,
2771                        effect_clause: vec![],
2772                        where_clause: vec![],
2773                        body: Box::new(block(3, vec![], None)),
2774                    },
2775                )],
2776            },
2777        );
2778        let out = gen(&module(vec![], vec![t]));
2779        assert!(out.contains("type Drawable interface {"), "got: {out}");
2780        assert!(out.contains("Draw()"), "got: {out}");
2781    }
2782
2783    #[test]
2784    fn enum_to_interface_and_structs() {
2785        let e = node(
2786            1,
2787            NodeKind::EnumDecl {
2788                annotations: vec![],
2789                visibility: Visibility::Public,
2790                name: ident("Shape"),
2791                generic_params: vec![],
2792                variants: vec![
2793                    node(
2794                        2,
2795                        NodeKind::EnumVariant {
2796                            name: ident("Circle"),
2797                            payload: EnumVariantPayload::Struct(vec![bock_ast::RecordDeclField {
2798                                id: 0,
2799                                span: span(),
2800                                name: ident("radius"),
2801                                ty: TypeExpr::Named {
2802                                    id: 0,
2803                                    span: span(),
2804                                    path: type_path(&["Float"]),
2805                                    args: vec![],
2806                                },
2807                                default: None,
2808                            }]),
2809                        },
2810                    ),
2811                    node(
2812                        3,
2813                        NodeKind::EnumVariant {
2814                            name: ident("None"),
2815                            payload: EnumVariantPayload::Unit,
2816                        },
2817                    ),
2818                ],
2819            },
2820        );
2821        let out = gen(&module(vec![], vec![e]));
2822        assert!(out.contains("type Shape interface {"), "got: {out}");
2823        assert!(out.contains("isShape()"), "got: {out}");
2824        assert!(out.contains("type ShapeCircle struct {"), "got: {out}");
2825        assert!(out.contains("Radius\tfloat64"), "got: {out}");
2826        assert!(out.contains("type ShapeNone struct{}"), "got: {out}");
2827        assert!(
2828            out.contains("func (ShapeCircle) isShape() {}"),
2829            "got: {out}"
2830        );
2831        assert!(out.contains("func (ShapeNone) isShape() {}"), "got: {out}");
2832    }
2833
2834    #[test]
2835    fn effects_as_interface_params() {
2836        let body = block(
2837            3,
2838            vec![node(
2839                4,
2840                NodeKind::LetBinding {
2841                    is_mut: false,
2842                    pattern: Box::new(bind_pat(5, "msg")),
2843                    ty: None,
2844                    value: Box::new(str_lit(6, "hello")),
2845                },
2846            )],
2847            Some(node(
2848                7,
2849                NodeKind::EffectOp {
2850                    effect: type_path(&["Log"]),
2851                    operation: ident("info"),
2852                    args: vec![AirArg {
2853                        label: None,
2854                        value: id_node(8, "msg"),
2855                    }],
2856                },
2857            )),
2858        );
2859        let f = node(
2860            1,
2861            NodeKind::FnDecl {
2862                annotations: vec![],
2863                visibility: Visibility::Public,
2864                is_async: false,
2865                name: ident("process"),
2866                generic_params: vec![],
2867                params: vec![param_node(2, "data")],
2868                return_type: None,
2869                effect_clause: vec![type_path(&["Log"]), type_path(&["Clock"])],
2870                where_clause: vec![],
2871                body: Box::new(body),
2872            },
2873        );
2874        let out = gen(&module(vec![], vec![f]));
2875        assert!(
2876            out.contains("func Process(data interface{}, log Log, clock Clock)"),
2877            "got: {out}"
2878        );
2879        assert!(out.contains("log.Info(msg)"), "got: {out}");
2880    }
2881
2882    #[test]
2883    fn generics_with_type_params() {
2884        let body = block(2, vec![], Some(id_node(3, "value")));
2885        let f = node(
2886            1,
2887            NodeKind::FnDecl {
2888                annotations: vec![],
2889                visibility: Visibility::Public,
2890                is_async: false,
2891                name: ident("identity"),
2892                generic_params: vec![bock_ast::GenericParam {
2893                    id: 10,
2894                    span: span(),
2895                    name: ident("T"),
2896                    bounds: vec![],
2897                }],
2898                params: vec![typed_param_node(2, "value", "T")],
2899                return_type: Some(Box::new(node(
2900                    4,
2901                    NodeKind::TypeNamed {
2902                        path: type_path(&["T"]),
2903                        args: vec![],
2904                    },
2905                ))),
2906                effect_clause: vec![],
2907                where_clause: vec![],
2908                body: Box::new(body),
2909            },
2910        );
2911        let out = gen(&module(vec![], vec![f]));
2912        assert!(
2913            out.contains("func Identity[T any](value T) T {"),
2914            "got: {out}"
2915        );
2916    }
2917
2918    #[test]
2919    fn generics_with_bounds() {
2920        let body = block(2, vec![], Some(id_node(3, "value")));
2921        let f = node(
2922            1,
2923            NodeKind::FnDecl {
2924                annotations: vec![],
2925                visibility: Visibility::Public,
2926                is_async: false,
2927                name: ident("constrained"),
2928                generic_params: vec![bock_ast::GenericParam {
2929                    id: 10,
2930                    span: span(),
2931                    name: ident("T"),
2932                    bounds: vec![type_path(&["Comparable"])],
2933                }],
2934                params: vec![typed_param_node(2, "value", "T")],
2935                return_type: Some(Box::new(node(
2936                    4,
2937                    NodeKind::TypeNamed {
2938                        path: type_path(&["T"]),
2939                        args: vec![],
2940                    },
2941                ))),
2942                effect_clause: vec![],
2943                where_clause: vec![],
2944                body: Box::new(body),
2945            },
2946        );
2947        let out = gen(&module(vec![], vec![f]));
2948        assert!(
2949            out.contains("func Constrained[T Comparable](value T) T {"),
2950            "got: {out}"
2951        );
2952    }
2953
2954    #[test]
2955    fn match_to_switch() {
2956        let m = node(
2957            1,
2958            NodeKind::Match {
2959                scrutinee: Box::new(id_node(2, "x")),
2960                arms: vec![
2961                    node(
2962                        3,
2963                        NodeKind::MatchArm {
2964                            pattern: Box::new(node(
2965                                4,
2966                                NodeKind::LiteralPat {
2967                                    lit: Literal::Int("1".into()),
2968                                },
2969                            )),
2970                            guard: None,
2971                            body: Box::new(block(5, vec![], Some(str_lit(6, "one")))),
2972                        },
2973                    ),
2974                    node(
2975                        7,
2976                        NodeKind::MatchArm {
2977                            pattern: Box::new(node(8, NodeKind::WildcardPat)),
2978                            guard: None,
2979                            body: Box::new(block(9, vec![], Some(str_lit(10, "other")))),
2980                        },
2981                    ),
2982                ],
2983            },
2984        );
2985        let out = gen(&module(vec![], vec![m]));
2986        assert!(out.contains("switch"), "got: {out}");
2987        assert!(out.contains("default:"), "got: {out}");
2988    }
2989
2990    #[test]
2991    fn match_arm_guard_emits_if() {
2992        let m = node(
2993            1,
2994            NodeKind::Match {
2995                scrutinee: Box::new(id_node(2, "x")),
2996                arms: vec![node(
2997                    3,
2998                    NodeKind::MatchArm {
2999                        pattern: Box::new(node(
3000                            4,
3001                            NodeKind::LiteralPat {
3002                                lit: Literal::Int("1".into()),
3003                            },
3004                        )),
3005                        guard: Some(Box::new(id_node(5, "ok"))),
3006                        body: Box::new(block(
3007                            6,
3008                            vec![node(7, NodeKind::Return { value: None })],
3009                            None,
3010                        )),
3011                    },
3012                )],
3013            },
3014        );
3015        let out = gen(&module(vec![], vec![m]));
3016        assert!(
3017            out.contains("if ok {"),
3018            "guard should emit real if-statement, got: {out}"
3019        );
3020        assert!(
3021            !out.contains("// guard"),
3022            "guard should not be a comment, got: {out}"
3023        );
3024    }
3025
3026    #[test]
3027    fn let_binding() {
3028        let l = node(
3029            1,
3030            NodeKind::LetBinding {
3031                is_mut: false,
3032                pattern: Box::new(bind_pat(2, "x")),
3033                ty: None,
3034                value: Box::new(int_lit(3, "42")),
3035            },
3036        );
3037        let out = gen(&module(vec![], vec![l]));
3038        assert!(out.contains("x := 42"), "got: {out}");
3039    }
3040
3041    #[test]
3042    fn let_binding_with_type() {
3043        let l = node(
3044            1,
3045            NodeKind::LetBinding {
3046                is_mut: false,
3047                pattern: Box::new(bind_pat(2, "x")),
3048                ty: Some(Box::new(node(
3049                    4,
3050                    NodeKind::TypeNamed {
3051                        path: type_path(&["Int"]),
3052                        args: vec![],
3053                    },
3054                ))),
3055                value: Box::new(int_lit(3, "42")),
3056            },
3057        );
3058        let out = gen(&module(vec![], vec![l]));
3059        assert!(out.contains("var x int64 = 42"), "got: {out}");
3060    }
3061
3062    #[test]
3063    fn if_else() {
3064        let stmt = node(
3065            1,
3066            NodeKind::If {
3067                let_pattern: None,
3068                condition: Box::new(bool_lit(2, true)),
3069                then_block: Box::new(block(3, vec![], Some(int_lit(4, "1")))),
3070                else_block: Some(Box::new(block(5, vec![], Some(int_lit(6, "0"))))),
3071            },
3072        );
3073        let out = gen(&module(vec![], vec![stmt]));
3074        assert!(out.contains("if true {"), "got: {out}");
3075        assert!(out.contains("} else {"), "got: {out}");
3076    }
3077
3078    #[test]
3079    fn for_loop() {
3080        let stmt = node(
3081            1,
3082            NodeKind::For {
3083                pattern: Box::new(bind_pat(2, "item")),
3084                iterable: Box::new(id_node(3, "items")),
3085                body: Box::new(block(4, vec![], None)),
3086            },
3087        );
3088        let out = gen(&module(vec![], vec![stmt]));
3089        assert!(out.contains("for _, item := range items {"), "got: {out}");
3090    }
3091
3092    #[test]
3093    fn while_loop() {
3094        let stmt = node(
3095            1,
3096            NodeKind::While {
3097                condition: Box::new(bool_lit(2, true)),
3098                body: Box::new(block(3, vec![], None)),
3099            },
3100        );
3101        let out = gen(&module(vec![], vec![stmt]));
3102        assert!(out.contains("for true {"), "got: {out}");
3103    }
3104
3105    #[test]
3106    fn infinite_loop() {
3107        let stmt = node(
3108            1,
3109            NodeKind::Loop {
3110                body: Box::new(block(2, vec![], None)),
3111            },
3112        );
3113        let out = gen(&module(vec![], vec![stmt]));
3114        assert!(out.contains("for {"), "got: {out}");
3115    }
3116
3117    #[test]
3118    fn string_interpolation() {
3119        let interp = node(
3120            1,
3121            NodeKind::Interpolation {
3122                parts: vec![
3123                    AirInterpolationPart::Literal("Hello, ".into()),
3124                    AirInterpolationPart::Expr(Box::new(id_node(2, "name"))),
3125                    AirInterpolationPart::Literal("!".into()),
3126                ],
3127            },
3128        );
3129        let out = gen(&module(vec![], vec![interp]));
3130        assert!(out.contains("fmt.Sprintf"), "got: {out}");
3131        assert!(out.contains("Hello, %v!"), "got: {out}");
3132        assert!(out.contains("import \"fmt\""), "got: {out}");
3133    }
3134
3135    #[test]
3136    fn record_construction() {
3137        let rc = node(
3138            1,
3139            NodeKind::RecordConstruct {
3140                path: type_path(&["Point"]),
3141                fields: vec![
3142                    AirRecordField {
3143                        name: ident("x"),
3144                        value: Some(Box::new(int_lit(2, "1"))),
3145                    },
3146                    AirRecordField {
3147                        name: ident("y"),
3148                        value: Some(Box::new(int_lit(3, "2"))),
3149                    },
3150                ],
3151                spread: None,
3152            },
3153        );
3154        let out = gen(&module(vec![], vec![rc]));
3155        assert!(out.contains("Point{X: 1, Y: 2}"), "got: {out}");
3156    }
3157
3158    #[test]
3159    fn list_literal() {
3160        let l = node(
3161            1,
3162            NodeKind::ListLiteral {
3163                elems: vec![int_lit(2, "1"), int_lit(3, "2"), int_lit(4, "3")],
3164            },
3165        );
3166        let out = gen(&module(vec![], vec![l]));
3167        assert!(out.contains("[]interface{}{1, 2, 3}"), "got: {out}");
3168    }
3169
3170    #[test]
3171    fn effect_decl_to_interface() {
3172        let ed = node(
3173            1,
3174            NodeKind::EffectDecl {
3175                annotations: vec![],
3176                visibility: Visibility::Public,
3177                name: ident("Logger"),
3178                generic_params: vec![],
3179                components: vec![],
3180                operations: vec![
3181                    node(
3182                        2,
3183                        NodeKind::FnDecl {
3184                            annotations: vec![],
3185                            visibility: Visibility::Public,
3186                            is_async: false,
3187                            name: ident("info"),
3188                            generic_params: vec![],
3189                            params: vec![typed_param_node(3, "msg", "String")],
3190                            return_type: None,
3191                            effect_clause: vec![],
3192                            where_clause: vec![],
3193                            body: Box::new(block(4, vec![], None)),
3194                        },
3195                    ),
3196                    node(
3197                        5,
3198                        NodeKind::FnDecl {
3199                            annotations: vec![],
3200                            visibility: Visibility::Public,
3201                            is_async: false,
3202                            name: ident("error"),
3203                            generic_params: vec![],
3204                            params: vec![typed_param_node(6, "msg", "String")],
3205                            return_type: None,
3206                            effect_clause: vec![],
3207                            where_clause: vec![],
3208                            body: Box::new(block(7, vec![], None)),
3209                        },
3210                    ),
3211                ],
3212            },
3213        );
3214        let out = gen(&module(vec![], vec![ed]));
3215        assert!(out.contains("type Logger interface {"), "got: {out}");
3216        assert!(out.contains("Info(string)"), "got: {out}");
3217        assert!(out.contains("Error(string)"), "got: {out}");
3218    }
3219
3220    #[test]
3221    fn result_construct_ok() {
3222        let rc = node(
3223            1,
3224            NodeKind::ResultConstruct {
3225                variant: ResultVariant::Ok,
3226                value: Some(Box::new(int_lit(2, "42"))),
3227            },
3228        );
3229        let out = gen(&module(vec![], vec![rc]));
3230        assert!(out.contains("42, nil"), "got: {out}");
3231    }
3232
3233    #[test]
3234    fn result_construct_err() {
3235        let rc = node(
3236            1,
3237            NodeKind::ResultConstruct {
3238                variant: ResultVariant::Err,
3239                value: Some(Box::new(str_lit(2, "failed"))),
3240            },
3241        );
3242        let out = gen(&module(vec![], vec![rc]));
3243        assert!(out.contains("nil, \"failed\""), "got: {out}");
3244    }
3245
3246    #[test]
3247    fn class_to_struct_with_methods() {
3248        let cls = node(
3249            1,
3250            NodeKind::ClassDecl {
3251                annotations: vec![],
3252                visibility: Visibility::Public,
3253                name: ident("Counter"),
3254                generic_params: vec![],
3255                base: None,
3256                traits: vec![],
3257                fields: vec![bock_ast::RecordDeclField {
3258                    id: 0,
3259                    span: span(),
3260                    name: ident("count"),
3261                    ty: TypeExpr::Named {
3262                        id: 0,
3263                        span: span(),
3264                        path: type_path(&["Int"]),
3265                        args: vec![],
3266                    },
3267                    default: None,
3268                }],
3269                methods: vec![node(
3270                    2,
3271                    NodeKind::FnDecl {
3272                        annotations: vec![],
3273                        visibility: Visibility::Public,
3274                        is_async: false,
3275                        name: ident("increment"),
3276                        generic_params: vec![],
3277                        params: vec![],
3278                        return_type: None,
3279                        effect_clause: vec![],
3280                        where_clause: vec![],
3281                        body: Box::new(block(3, vec![], None)),
3282                    },
3283                )],
3284            },
3285        );
3286        let out = gen(&module(vec![], vec![cls]));
3287        assert!(out.contains("type Counter struct {"), "got: {out}");
3288        assert!(out.contains("Count\tint64"), "got: {out}");
3289        assert!(out.contains("func NewCounter("), "got: {out}");
3290        assert!(out.contains("func (c *Counter) Increment()"), "got: {out}");
3291    }
3292
3293    #[test]
3294    fn lambda_expression() {
3295        let lam = node(
3296            1,
3297            NodeKind::Lambda {
3298                params: vec![param_node(2, "x")],
3299                body: Box::new(node(
3300                    3,
3301                    NodeKind::BinaryOp {
3302                        op: BinOp::Mul,
3303                        left: Box::new(id_node(4, "x")),
3304                        right: Box::new(int_lit(5, "2")),
3305                    },
3306                )),
3307            },
3308        );
3309        let out = gen(&module(vec![], vec![lam]));
3310        assert!(
3311            out.contains("func(x interface{}) interface{} { return (x * 2) }"),
3312            "got: {out}"
3313        );
3314    }
3315
3316    #[test]
3317    fn impl_block_methods() {
3318        let imp = node(
3319            1,
3320            NodeKind::ImplBlock {
3321                annotations: vec![],
3322                generic_params: vec![],
3323                trait_path: None,
3324                target: Box::new(node(
3325                    2,
3326                    NodeKind::TypeNamed {
3327                        path: type_path(&["Point"]),
3328                        args: vec![],
3329                    },
3330                )),
3331                where_clause: vec![],
3332                methods: vec![node(
3333                    3,
3334                    NodeKind::FnDecl {
3335                        annotations: vec![],
3336                        visibility: Visibility::Public,
3337                        is_async: false,
3338                        name: ident("distance"),
3339                        generic_params: vec![],
3340                        params: vec![],
3341                        return_type: Some(Box::new(node(
3342                            4,
3343                            NodeKind::TypeNamed {
3344                                path: type_path(&["Float"]),
3345                                args: vec![],
3346                            },
3347                        ))),
3348                        effect_clause: vec![],
3349                        where_clause: vec![],
3350                        body: Box::new(block(5, vec![], Some(int_lit(6, "0")))),
3351                    },
3352                )],
3353            },
3354        );
3355        let out = gen(&module(vec![], vec![imp]));
3356        assert!(
3357            out.contains("func (p *Point) Distance() float64 {"),
3358            "got: {out}"
3359        );
3360    }
3361
3362    #[test]
3363    fn concurrency_goroutine() {
3364        // Async function → goroutine pattern with channel.
3365        // The await expression maps to channel receive.
3366        let body = block(
3367            3,
3368            vec![],
3369            Some(node(
3370                4,
3371                NodeKind::Await {
3372                    expr: Box::new(id_node(5, "ch")),
3373                },
3374            )),
3375        );
3376        let f = node(
3377            1,
3378            NodeKind::FnDecl {
3379                annotations: vec![],
3380                visibility: Visibility::Public,
3381                is_async: true,
3382                name: ident("fetchData"),
3383                generic_params: vec![],
3384                params: vec![],
3385                return_type: None,
3386                effect_clause: vec![],
3387                where_clause: vec![],
3388                body: Box::new(body),
3389            },
3390        );
3391        let out = gen(&module(vec![], vec![f]));
3392        assert!(out.contains("func FetchData()"), "got: {out}");
3393        assert!(out.contains("<-ch"), "got: {out}");
3394    }
3395
3396    #[test]
3397    fn async_fn_emits_goroutine_wrapper() {
3398        // Async function with Int return → sync body + FnAsync wrapper
3399        // returning `<-chan int`.
3400        let body = block(3, vec![], Some(int_lit(4, "42")));
3401        let f = node(
3402            1,
3403            NodeKind::FnDecl {
3404                annotations: vec![],
3405                visibility: Visibility::Public,
3406                is_async: true,
3407                name: ident("task1"),
3408                generic_params: vec![],
3409                params: vec![],
3410                return_type: Some(Box::new(node(
3411                    5,
3412                    NodeKind::TypeNamed {
3413                        path: type_path(&["Int"]),
3414                        args: vec![],
3415                    },
3416                ))),
3417                effect_clause: vec![],
3418                where_clause: vec![],
3419                body: Box::new(body),
3420            },
3421        );
3422        let out = gen(&module(vec![], vec![f]));
3423        assert!(
3424            out.contains("func Task1() int64 {"),
3425            "sync body missing: {out}"
3426        );
3427        assert!(
3428            out.contains("func Task1Async() <-chan int64 {"),
3429            "async wrapper missing: {out}"
3430        );
3431        assert!(out.contains("__ch := make(chan int64, 1)"), "got: {out}");
3432        assert!(out.contains("go func() {"), "got: {out}");
3433        assert!(out.contains("__ch <- Task1()"), "got: {out}");
3434        assert!(out.contains("return __ch"), "got: {out}");
3435    }
3436
3437    #[test]
3438    fn async_main_no_wrapper() {
3439        // main is Go's entry — skip the wrapper to avoid dead code.
3440        let body = block(2, vec![], None);
3441        let f = node(
3442            1,
3443            NodeKind::FnDecl {
3444                annotations: vec![],
3445                visibility: Visibility::Private,
3446                is_async: true,
3447                name: ident("main"),
3448                generic_params: vec![],
3449                params: vec![],
3450                return_type: None,
3451                effect_clause: vec![],
3452                where_clause: vec![],
3453                body: Box::new(body),
3454            },
3455        );
3456        let out = gen(&module(vec![], vec![f]));
3457        assert!(out.contains("func main() {"), "got: {out}");
3458        assert!(!out.contains("mainAsync"), "got: {out}");
3459    }
3460
3461    #[test]
3462    fn async_call_rewritten_to_async_wrapper() {
3463        // Calling `task1()` from another async fn should route through
3464        // `Task1Async()` so callers can `await` (= `<-`) the channel.
3465        let task1 = node(
3466            10,
3467            NodeKind::FnDecl {
3468                annotations: vec![],
3469                visibility: Visibility::Public,
3470                is_async: true,
3471                name: ident("task1"),
3472                generic_params: vec![],
3473                params: vec![],
3474                return_type: Some(Box::new(node(
3475                    11,
3476                    NodeKind::TypeNamed {
3477                        path: type_path(&["Int"]),
3478                        args: vec![],
3479                    },
3480                ))),
3481                effect_clause: vec![],
3482                where_clause: vec![],
3483                body: Box::new(block(12, vec![], Some(int_lit(13, "1")))),
3484            },
3485        );
3486        // caller body: let a = task1(); let b = task1(); await a; await b
3487        let call_task1 = |id: u32| {
3488            node(
3489                id,
3490                NodeKind::Call {
3491                    callee: Box::new(id_node(id + 1, "task1")),
3492                    args: vec![],
3493                    type_args: vec![],
3494                },
3495            )
3496        };
3497        let let_stmt = |id: u32, name: &str, val: AIRNode| {
3498            node(
3499                id,
3500                NodeKind::LetBinding {
3501                    is_mut: false,
3502                    pattern: Box::new(bind_pat(id + 1, name)),
3503                    ty: None,
3504                    value: Box::new(val),
3505                },
3506            )
3507        };
3508        let await_id = |id: u32, name: &str| {
3509            node(
3510                id,
3511                NodeKind::Await {
3512                    expr: Box::new(id_node(id + 1, name)),
3513                },
3514            )
3515        };
3516        let caller_body = block(
3517            20,
3518            vec![
3519                let_stmt(30, "a", call_task1(31)),
3520                let_stmt(40, "b", call_task1(41)),
3521                let_stmt(50, "ra", await_id(51, "a")),
3522                let_stmt(60, "rb", await_id(61, "b")),
3523            ],
3524            None,
3525        );
3526        let caller = node(
3527            100,
3528            NodeKind::FnDecl {
3529                annotations: vec![],
3530                visibility: Visibility::Private,
3531                is_async: true,
3532                name: ident("run"),
3533                generic_params: vec![],
3534                params: vec![],
3535                return_type: None,
3536                effect_clause: vec![],
3537                where_clause: vec![],
3538                body: Box::new(caller_body),
3539            },
3540        );
3541        let out = gen(&module(vec![], vec![task1, caller]));
3542        // Concurrent goroutines: both bindings start channels.
3543        assert!(out.contains("a := Task1Async()"), "got: {out}");
3544        assert!(out.contains("b := Task1Async()"), "got: {out}");
3545        // Awaits receive from the channels.
3546        assert!(out.contains("ra := <-a"), "got: {out}");
3547        assert!(out.contains("rb := <-b"), "got: {out}");
3548    }
3549
3550    #[test]
3551    fn break_continue() {
3552        let brk = node(1, NodeKind::Break { value: None });
3553        let cont = node(2, NodeKind::Continue);
3554        let out = gen(&module(vec![], vec![brk, cont]));
3555        assert!(out.contains("break"), "got: {out}");
3556        assert!(out.contains("continue"), "got: {out}");
3557    }
3558
3559    #[test]
3560    fn guard_statement() {
3561        let g = node(
3562            1,
3563            NodeKind::Guard {
3564                let_pattern: None,
3565                condition: Box::new(bool_lit(2, true)),
3566                else_block: Box::new(block(
3567                    3,
3568                    vec![node(4, NodeKind::Return { value: None })],
3569                    None,
3570                )),
3571            },
3572        );
3573        let out = gen(&module(vec![], vec![g]));
3574        assert!(out.contains("if !(true)"), "got: {out}");
3575    }
3576
3577    #[test]
3578    fn ownership_erased() {
3579        let borrow = node(
3580            1,
3581            NodeKind::Borrow {
3582                expr: Box::new(id_node(2, "x")),
3583            },
3584        );
3585        let mv = node(
3586            3,
3587            NodeKind::Move {
3588                expr: Box::new(id_node(4, "y")),
3589            },
3590        );
3591        let out = gen(&module(vec![], vec![borrow, mv]));
3592        assert!(out.contains("x"), "got: {out}");
3593        assert!(out.contains("y"), "got: {out}");
3594        // Should NOT contain borrow/move keywords.
3595        assert!(!out.contains("&x"), "got: {out}");
3596    }
3597
3598    #[test]
3599    fn type_mapping() {
3600        let ctx = GoEmitCtx::new();
3601        assert_eq!(ctx.map_type_name("Int"), "int64");
3602        assert_eq!(ctx.map_type_name("Float"), "float64");
3603        assert_eq!(ctx.map_type_name("Bool"), "bool");
3604        assert_eq!(ctx.map_type_name("String"), "string");
3605        assert_eq!(ctx.map_type_name("Void"), "struct{}");
3606        assert_eq!(ctx.map_type_name("Any"), "interface{}");
3607    }
3608
3609    #[test]
3610    fn naming_conventions() {
3611        assert_eq!(to_camel_case("hello_world"), "helloWorld");
3612        assert_eq!(to_camel_case("HelloWorld"), "helloWorld");
3613        assert_eq!(to_camel_case("already"), "already");
3614        assert_eq!(to_pascal_case("hello_world"), "HelloWorld");
3615        assert_eq!(to_pascal_case("helloWorld"), "HelloWorld");
3616        assert_eq!(to_pascal_case("Already"), "Already");
3617    }
3618
3619    #[test]
3620    fn escape_go_string_special_chars() {
3621        assert_eq!(escape_go_string("hello\nworld"), "hello\\nworld");
3622        assert_eq!(escape_go_string("tab\there"), "tab\\there");
3623        assert_eq!(escape_go_string("quote\"here"), "quote\\\"here");
3624    }
3625
3626    // ── End-to-end: syntax validation ───────────────────────────────────────
3627
3628    #[test]
3629    #[ignore] // requires `go` to be installed
3630    fn generated_go_passes_vet() {
3631        let body = block(
3632            2,
3633            vec![],
3634            Some(node(
3635                3,
3636                NodeKind::Interpolation {
3637                    parts: vec![
3638                        AirInterpolationPart::Literal("Hello, ".into()),
3639                        AirInterpolationPart::Expr(Box::new(id_node(4, "name"))),
3640                    ],
3641                },
3642            )),
3643        );
3644        let f = node(
3645            1,
3646            NodeKind::FnDecl {
3647                annotations: vec![],
3648                visibility: Visibility::Public,
3649                is_async: false,
3650                name: ident("greet"),
3651                generic_params: vec![],
3652                params: vec![typed_param_node(5, "name", "String")],
3653                return_type: Some(Box::new(node(
3654                    6,
3655                    NodeKind::TypeNamed {
3656                        path: type_path(&["String"]),
3657                        args: vec![],
3658                    },
3659                ))),
3660                effect_clause: vec![],
3661                where_clause: vec![],
3662                body: Box::new(body),
3663            },
3664        );
3665        let code = gen(&module(vec![], vec![f]));
3666
3667        // Write to temp file and run go vet.
3668        let dir = std::env::temp_dir().join("bock_go_test");
3669        let _ = std::fs::create_dir_all(&dir);
3670        let file_path = dir.join("output.go");
3671        std::fs::write(&file_path, &code).unwrap();
3672
3673        let output = std::process::Command::new("go")
3674            .args(["vet", file_path.to_str().unwrap()])
3675            .output();
3676        match output {
3677            Ok(o) => {
3678                if !o.status.success() {
3679                    let stderr = String::from_utf8_lossy(&o.stderr);
3680                    panic!("go vet failed:\n{stderr}\n\nGenerated code:\n{code}");
3681                }
3682            }
3683            Err(e) => {
3684                panic!("Failed to run go vet: {e}");
3685            }
3686        }
3687        let _ = std::fs::remove_dir_all(&dir);
3688    }
3689
3690    #[test]
3691    #[ignore] // requires `go` to be installed
3692    fn generated_go_compiles_and_runs() {
3693        // Build a complete Go program that prints "42".
3694        let body = block(
3695            2,
3696            vec![node(
3697                3,
3698                NodeKind::LetBinding {
3699                    is_mut: false,
3700                    pattern: Box::new(bind_pat(4, "x")),
3701                    ty: None,
3702                    value: Box::new(int_lit(5, "42")),
3703                },
3704            )],
3705            None,
3706        );
3707        let main_fn = node(
3708            1,
3709            NodeKind::FnDecl {
3710                annotations: vec![],
3711                visibility: Visibility::Private,
3712                is_async: false,
3713                name: ident("main"),
3714                generic_params: vec![],
3715                params: vec![],
3716                return_type: None,
3717                effect_clause: vec![],
3718                where_clause: vec![],
3719                body: Box::new(body),
3720            },
3721        );
3722        let code = gen(&module(vec![], vec![main_fn]));
3723
3724        let dir = std::env::temp_dir().join("bock_go_run_test");
3725        let _ = std::fs::create_dir_all(&dir);
3726        let file_path = dir.join("main.go");
3727        std::fs::write(&file_path, &code).unwrap();
3728
3729        let output = std::process::Command::new("go")
3730            .args(["build", file_path.to_str().unwrap()])
3731            .current_dir(&dir)
3732            .output();
3733        match output {
3734            Ok(o) => {
3735                if !o.status.success() {
3736                    let stderr = String::from_utf8_lossy(&o.stderr);
3737                    panic!("go build failed:\n{stderr}\n\nGenerated code:\n{code}");
3738                }
3739            }
3740            Err(e) => {
3741                panic!("Failed to run go build: {e}");
3742            }
3743        }
3744        let _ = std::fs::remove_dir_all(&dir);
3745    }
3746
3747    #[test]
3748    fn expr_match_no_unused_var() {
3749        // Expression-position match should not emit unused `__v`.
3750        let match_expr = node(
3751            1,
3752            NodeKind::Match {
3753                scrutinee: Box::new(id_node(2, "x")),
3754                arms: vec![
3755                    node(
3756                        3,
3757                        NodeKind::MatchArm {
3758                            pattern: Box::new(node(
3759                                4,
3760                                NodeKind::LiteralPat {
3761                                    lit: Literal::Int("1".into()),
3762                                },
3763                            )),
3764                            guard: None,
3765                            body: Box::new(block(5, vec![], Some(str_lit(6, "one")))),
3766                        },
3767                    ),
3768                    node(
3769                        7,
3770                        NodeKind::MatchArm {
3771                            pattern: Box::new(node(8, NodeKind::WildcardPat)),
3772                            guard: None,
3773                            body: Box::new(block(9, vec![], Some(str_lit(10, "other")))),
3774                        },
3775                    ),
3776                ],
3777            },
3778        );
3779        // Emit in expression context via a let binding.
3780        let let_node = node(
3781            20,
3782            NodeKind::LetBinding {
3783                is_mut: false,
3784                pattern: Box::new(bind_pat(21, "result")),
3785                ty: None,
3786                value: Box::new(match_expr),
3787            },
3788        );
3789        let out = gen(&module(vec![], vec![let_node]));
3790        assert!(
3791            !out.contains("__v"),
3792            "expression-position match should not emit __v, got: {out}"
3793        );
3794        assert!(
3795            out.contains("switch x"),
3796            "should emit switch with scrutinee directly, got: {out}"
3797        );
3798    }
3799
3800    // ── Prelude function mapping tests ──────────────────────────────────────
3801
3802    /// Helper: generate Go for a module with a `main` function containing a single call.
3803    fn gen_prelude_call(func_name: &str, arg: AIRNode) -> String {
3804        let call = node(
3805            10,
3806            NodeKind::Call {
3807                callee: Box::new(id_node(11, func_name)),
3808                args: vec![AirArg {
3809                    label: None,
3810                    value: arg,
3811                }],
3812                type_args: vec![],
3813            },
3814        );
3815        let body = block(2, vec![call], None);
3816        let f = node(
3817            1,
3818            NodeKind::FnDecl {
3819                name: ident("main"),
3820                params: vec![],
3821                return_type: None,
3822                body: Box::new(body),
3823                generic_params: vec![],
3824                visibility: Visibility::Private,
3825                annotations: vec![],
3826                effect_clause: vec![],
3827                where_clause: vec![],
3828                is_async: false,
3829            },
3830        );
3831        gen(&module(vec![], vec![f]))
3832    }
3833
3834    /// Helper: generate Go for a nullary prelude call (no args).
3835    fn gen_prelude_call_no_args(func_name: &str) -> String {
3836        let call = node(
3837            10,
3838            NodeKind::Call {
3839                callee: Box::new(id_node(11, func_name)),
3840                args: vec![],
3841                type_args: vec![],
3842            },
3843        );
3844        let body = block(2, vec![call], None);
3845        let f = node(
3846            1,
3847            NodeKind::FnDecl {
3848                name: ident("main"),
3849                params: vec![],
3850                return_type: None,
3851                body: Box::new(body),
3852                generic_params: vec![],
3853                visibility: Visibility::Private,
3854                annotations: vec![],
3855                effect_clause: vec![],
3856                where_clause: vec![],
3857                is_async: false,
3858            },
3859        );
3860        gen(&module(vec![], vec![f]))
3861    }
3862
3863    #[test]
3864    fn prelude_println_maps_to_fmt_println() {
3865        let out = gen_prelude_call("println", str_lit(12, "hello"));
3866        assert!(
3867            out.contains("fmt.Println("),
3868            "println should map to fmt.Println, got: {out}"
3869        );
3870        assert!(
3871            !out.contains("println("),
3872            "should not emit bare println(, got: {out}"
3873        );
3874    }
3875
3876    #[test]
3877    fn prelude_print_maps_to_fmt_print() {
3878        let out = gen_prelude_call("print", str_lit(12, "hello"));
3879        assert!(
3880            out.contains("fmt.Print("),
3881            "print should map to fmt.Print, got: {out}"
3882        );
3883    }
3884
3885    #[test]
3886    fn prelude_debug_maps_to_fmt_printf() {
3887        let out = gen_prelude_call("debug", str_lit(12, "val"));
3888        assert!(
3889            out.contains("fmt.Printf(\"%+v\\n\", "),
3890            "debug should map to fmt.Printf, got: {out}"
3891        );
3892    }
3893
3894    #[test]
3895    fn prelude_assert_maps_to_panic() {
3896        let out = gen_prelude_call("assert", bool_lit(12, true));
3897        assert!(
3898            out.contains("if !true { panic(\"assertion failed\") }"),
3899            "assert should map to if-panic, got: {out}"
3900        );
3901    }
3902
3903    #[test]
3904    fn prelude_todo_maps_to_panic_not_implemented() {
3905        let out = gen_prelude_call_no_args("todo");
3906        assert!(
3907            out.contains("panic(\"not implemented\")"),
3908            "todo should map to panic, got: {out}"
3909        );
3910    }
3911
3912    #[test]
3913    fn prelude_unreachable_maps_to_panic_unreachable() {
3914        let out = gen_prelude_call_no_args("unreachable");
3915        assert!(
3916            out.contains("panic(\"unreachable\")"),
3917            "unreachable should map to panic, got: {out}"
3918        );
3919    }
3920
3921    #[test]
3922    fn non_prelude_call_passes_through() {
3923        let out = gen_prelude_call("my_custom_func", str_lit(12, "arg"));
3924        assert!(
3925            out.contains("myCustomFunc("),
3926            "non-prelude call should use camelCase, got: {out}"
3927        );
3928    }
3929
3930    #[test]
3931    fn handling_block_passes_handlers_to_effectful_call() {
3932        use bock_air::AirHandlerPair;
3933
3934        let effect_decl = node(
3935            1,
3936            NodeKind::EffectDecl {
3937                annotations: vec![],
3938                visibility: Visibility::Public,
3939                name: ident("Logger"),
3940                generic_params: vec![],
3941                components: vec![],
3942                operations: vec![node(
3943                    2,
3944                    NodeKind::FnDecl {
3945                        annotations: vec![],
3946                        visibility: Visibility::Public,
3947                        is_async: false,
3948                        name: ident("log"),
3949                        generic_params: vec![],
3950                        params: vec![typed_param_node(3, "msg", "String")],
3951                        return_type: None,
3952                        effect_clause: vec![],
3953                        where_clause: vec![],
3954                        body: Box::new(block(4, vec![], None)),
3955                    },
3956                )],
3957            },
3958        );
3959
3960        let inner_fn = node(
3961            10,
3962            NodeKind::FnDecl {
3963                annotations: vec![],
3964                visibility: Visibility::Private,
3965                is_async: false,
3966                name: ident("inner"),
3967                generic_params: vec![],
3968                params: vec![],
3969                return_type: None,
3970                effect_clause: vec![type_path(&["Logger"])],
3971                where_clause: vec![],
3972                body: Box::new(block(12, vec![], Some(str_lit(13, "hello")))),
3973            },
3974        );
3975
3976        let call_inner = node(
3977            20,
3978            NodeKind::Call {
3979                callee: Box::new(id_node(21, "inner")),
3980                args: vec![],
3981                type_args: vec![],
3982            },
3983        );
3984        let handling = node(
3985            30,
3986            NodeKind::HandlingBlock {
3987                handlers: vec![AirHandlerPair {
3988                    effect: type_path(&["Logger"]),
3989                    handler: Box::new(node(
3990                        31,
3991                        NodeKind::Call {
3992                            callee: Box::new(id_node(32, "StdoutLogger")),
3993                            args: vec![],
3994                            type_args: vec![],
3995                        },
3996                    )),
3997                }],
3998                body: Box::new(block(33, vec![], Some(call_inner))),
3999            },
4000        );
4001        let main_fn = node(
4002            40,
4003            NodeKind::FnDecl {
4004                annotations: vec![],
4005                visibility: Visibility::Private,
4006                is_async: false,
4007                name: ident("main"),
4008                generic_params: vec![],
4009                params: vec![],
4010                return_type: None,
4011                effect_clause: vec![],
4012                where_clause: vec![],
4013                body: Box::new(block(41, vec![handling], None)),
4014            },
4015        );
4016
4017        let out = gen(&module(vec![], vec![effect_decl, inner_fn, main_fn]));
4018        // Go: inner(__logger)
4019        assert!(
4020            out.contains("inner(__logger)"),
4021            "handling block should pass handler to effectful call, got: {out}"
4022        );
4023        assert!(
4024            out.contains("__logger := stdoutLogger()"),
4025            "handling block should instantiate handler, got: {out}"
4026        );
4027    }
4028
4029    // ── C.8 Go effect codegen polish tests ──────────────────────────────────
4030
4031    fn type_named_node(id: u32, name: &str) -> AIRNode {
4032        node(
4033            id,
4034            NodeKind::TypeNamed {
4035                path: type_path(&[name]),
4036                args: vec![],
4037            },
4038        )
4039    }
4040
4041    /// Effect interface: Void-returning operations emit no return type.
4042    #[test]
4043    fn effect_interface_drops_void_return_type() {
4044        let void_op = node(
4045            2,
4046            NodeKind::FnDecl {
4047                annotations: vec![],
4048                visibility: Visibility::Public,
4049                is_async: false,
4050                name: ident("log"),
4051                generic_params: vec![],
4052                params: vec![typed_param_node(3, "msg", "String")],
4053                return_type: Some(Box::new(type_named_node(4, "Void"))),
4054                effect_clause: vec![],
4055                where_clause: vec![],
4056                body: Box::new(block(5, vec![], None)),
4057            },
4058        );
4059        let effect_decl = node(
4060            1,
4061            NodeKind::EffectDecl {
4062                annotations: vec![],
4063                visibility: Visibility::Public,
4064                name: ident("Logger"),
4065                generic_params: vec![],
4066                components: vec![],
4067                operations: vec![void_op],
4068            },
4069        );
4070        let out = gen(&module(vec![], vec![effect_decl]));
4071        assert!(
4072            out.contains("type Logger interface {"),
4073            "should emit interface, got: {out}"
4074        );
4075        assert!(
4076            out.contains("Log(string)\n"),
4077            "Void op should have no return type, got: {out}"
4078        );
4079        assert!(
4080            !out.contains("Log(string) struct{}"),
4081            "Void op should NOT emit struct{{}} return, got: {out}"
4082        );
4083    }
4084
4085    /// Public effectful function: Void return type is dropped in Go signature.
4086    #[test]
4087    fn fn_decl_drops_void_return_type() {
4088        let f = node(
4089            10,
4090            NodeKind::FnDecl {
4091                annotations: vec![],
4092                visibility: Visibility::Public,
4093                is_async: false,
4094                name: ident("do_thing"),
4095                generic_params: vec![],
4096                params: vec![],
4097                return_type: Some(Box::new(type_named_node(11, "Void"))),
4098                effect_clause: vec![],
4099                where_clause: vec![],
4100                body: Box::new(block(12, vec![], None)),
4101            },
4102        );
4103        let out = gen(&module(vec![], vec![f]));
4104        assert!(
4105            out.contains("func DoThing() {"),
4106            "Void fn should have no return type, got: {out}"
4107        );
4108        assert!(
4109            !out.contains("DoThing() struct{}"),
4110            "should not emit struct{{}} return, got: {out}"
4111        );
4112    }
4113
4114    /// Public function call sites emit PascalCase matching their definition.
4115    #[test]
4116    fn call_site_uses_pascal_case_for_public_fn() {
4117        let pub_fn = node(
4118            10,
4119            NodeKind::FnDecl {
4120                annotations: vec![],
4121                visibility: Visibility::Public,
4122                is_async: false,
4123                name: ident("do_thing"),
4124                generic_params: vec![],
4125                params: vec![],
4126                return_type: None,
4127                effect_clause: vec![],
4128                where_clause: vec![],
4129                body: Box::new(block(12, vec![], None)),
4130            },
4131        );
4132        let call = node(
4133            20,
4134            NodeKind::Call {
4135                callee: Box::new(id_node(21, "do_thing")),
4136                args: vec![],
4137                type_args: vec![],
4138            },
4139        );
4140        let main_fn = node(
4141            30,
4142            NodeKind::FnDecl {
4143                annotations: vec![],
4144                visibility: Visibility::Private,
4145                is_async: false,
4146                name: ident("main"),
4147                generic_params: vec![],
4148                params: vec![],
4149                return_type: None,
4150                effect_clause: vec![],
4151                where_clause: vec![],
4152                body: Box::new(block(31, vec![], Some(call))),
4153            },
4154        );
4155        let out = gen(&module(vec![], vec![pub_fn, main_fn]));
4156        assert!(
4157            out.contains("DoThing()"),
4158            "call to public fn should be PascalCase, got: {out}"
4159        );
4160        assert!(
4161            !out.contains("doThing()"),
4162            "call should NOT use camelCase for public fn, got: {out}"
4163        );
4164    }
4165
4166    /// Trait/effect impl blocks use value receivers so `Handler{}` satisfies the interface.
4167    #[test]
4168    fn impl_block_methods_use_value_receivers() {
4169        let record_decl = node(
4170            1,
4171            NodeKind::RecordDecl {
4172                annotations: vec![],
4173                visibility: Visibility::Public,
4174                name: ident("StdoutLogger"),
4175                generic_params: vec![],
4176                fields: vec![],
4177            },
4178        );
4179        let method = node(
4180            10,
4181            NodeKind::FnDecl {
4182                annotations: vec![],
4183                visibility: Visibility::Public,
4184                is_async: false,
4185                name: ident("log"),
4186                generic_params: vec![],
4187                params: vec![typed_param_node(11, "msg", "String")],
4188                return_type: Some(Box::new(type_named_node(12, "Void"))),
4189                effect_clause: vec![],
4190                where_clause: vec![],
4191                body: Box::new(block(13, vec![], None)),
4192            },
4193        );
4194        let impl_block = node(
4195            20,
4196            NodeKind::ImplBlock {
4197                annotations: vec![],
4198                target: Box::new(type_named_node(21, "StdoutLogger")),
4199                trait_path: Some(type_path(&["Logger"])),
4200                generic_params: vec![],
4201                where_clause: vec![],
4202                methods: vec![method],
4203            },
4204        );
4205        let out = gen(&module(vec![], vec![record_decl, impl_block]));
4206        assert!(
4207            out.contains("func (s StdoutLogger) Log("),
4208            "impl method should use value receiver, got: {out}"
4209        );
4210        assert!(
4211            !out.contains("func (s *StdoutLogger) Log("),
4212            "impl method should NOT use pointer receiver, got: {out}"
4213        );
4214    }
4215
4216    /// Module-level `handle` declares a var AND registers it so module-level
4217    /// calls to effectful functions pick it up.
4218    #[test]
4219    fn module_handle_registers_handler_for_calls() {
4220        use bock_air::AirHandlerPair;
4221        let _ = AirHandlerPair {
4222            effect: type_path(&["Logger"]),
4223            handler: Box::new(str_lit(999, "placeholder")),
4224        };
4225
4226        let effect_decl = node(
4227            1,
4228            NodeKind::EffectDecl {
4229                annotations: vec![],
4230                visibility: Visibility::Public,
4231                name: ident("Logger"),
4232                generic_params: vec![],
4233                components: vec![],
4234                operations: vec![node(
4235                    2,
4236                    NodeKind::FnDecl {
4237                        annotations: vec![],
4238                        visibility: Visibility::Public,
4239                        is_async: false,
4240                        name: ident("log"),
4241                        generic_params: vec![],
4242                        params: vec![typed_param_node(3, "msg", "String")],
4243                        return_type: Some(Box::new(type_named_node(4, "Void"))),
4244                        effect_clause: vec![],
4245                        where_clause: vec![],
4246                        body: Box::new(block(5, vec![], None)),
4247                    },
4248                )],
4249            },
4250        );
4251
4252        let effectful_fn = node(
4253            10,
4254            NodeKind::FnDecl {
4255                annotations: vec![],
4256                visibility: Visibility::Public,
4257                is_async: false,
4258                name: ident("do_log"),
4259                generic_params: vec![],
4260                params: vec![],
4261                return_type: None,
4262                effect_clause: vec![type_path(&["Logger"])],
4263                where_clause: vec![],
4264                body: Box::new(block(11, vec![], None)),
4265            },
4266        );
4267
4268        let module_handle = node(
4269            20,
4270            NodeKind::ModuleHandle {
4271                effect: type_path(&["Logger"]),
4272                handler: Box::new(node(
4273                    21,
4274                    NodeKind::Call {
4275                        callee: Box::new(id_node(22, "StdoutLogger")),
4276                        args: vec![],
4277                        type_args: vec![],
4278                    },
4279                )),
4280            },
4281        );
4282
4283        let main_call = node(
4284            30,
4285            NodeKind::Call {
4286                callee: Box::new(id_node(31, "do_log")),
4287                args: vec![],
4288                type_args: vec![],
4289            },
4290        );
4291        let main_fn = node(
4292            40,
4293            NodeKind::FnDecl {
4294                annotations: vec![],
4295                visibility: Visibility::Private,
4296                is_async: false,
4297                name: ident("main"),
4298                generic_params: vec![],
4299                params: vec![],
4300                return_type: None,
4301                effect_clause: vec![],
4302                where_clause: vec![],
4303                body: Box::new(block(41, vec![], Some(main_call))),
4304            },
4305        );
4306
4307        let out = gen(&module(
4308            vec![],
4309            vec![effect_decl, effectful_fn, module_handle, main_fn],
4310        ));
4311        assert!(
4312            out.contains("var __logger Logger = stdoutLogger()"),
4313            "module handle should declare var, got: {out}"
4314        );
4315        assert!(
4316            out.contains("DoLog(__logger)"),
4317            "module-level call should receive __logger, got: {out}"
4318        );
4319    }
4320
4321    /// Handling block suppresses Go "declared but not used" errors for handler vars.
4322    #[test]
4323    fn handling_block_emits_unused_suppression() {
4324        use bock_air::AirHandlerPair;
4325        let effect_decl = node(
4326            1,
4327            NodeKind::EffectDecl {
4328                annotations: vec![],
4329                visibility: Visibility::Public,
4330                name: ident("Logger"),
4331                generic_params: vec![],
4332                components: vec![],
4333                operations: vec![node(
4334                    2,
4335                    NodeKind::FnDecl {
4336                        annotations: vec![],
4337                        visibility: Visibility::Public,
4338                        is_async: false,
4339                        name: ident("log"),
4340                        generic_params: vec![],
4341                        params: vec![typed_param_node(3, "msg", "String")],
4342                        return_type: Some(Box::new(type_named_node(4, "Void"))),
4343                        effect_clause: vec![],
4344                        where_clause: vec![],
4345                        body: Box::new(block(5, vec![], None)),
4346                    },
4347                )],
4348            },
4349        );
4350        let handling = node(
4351            30,
4352            NodeKind::HandlingBlock {
4353                handlers: vec![AirHandlerPair {
4354                    effect: type_path(&["Logger"]),
4355                    handler: Box::new(node(
4356                        31,
4357                        NodeKind::Call {
4358                            callee: Box::new(id_node(32, "StdoutLogger")),
4359                            args: vec![],
4360                            type_args: vec![],
4361                        },
4362                    )),
4363                }],
4364                body: Box::new(block(33, vec![], Some(str_lit(34, "body")))),
4365            },
4366        );
4367        let main_fn = node(
4368            40,
4369            NodeKind::FnDecl {
4370                annotations: vec![],
4371                visibility: Visibility::Private,
4372                is_async: false,
4373                name: ident("main"),
4374                generic_params: vec![],
4375                params: vec![],
4376                return_type: None,
4377                effect_clause: vec![],
4378                where_clause: vec![],
4379                body: Box::new(block(41, vec![handling], None)),
4380            },
4381        );
4382        let out = gen(&module(vec![], vec![effect_decl, main_fn]));
4383        assert!(
4384            out.contains("_ = __logger"),
4385            "should suppress unused-var error for handler, got: {out}"
4386        );
4387    }
4388
4389    /// Void effect operations (e.g., log) are not wrapped in `return` when a
4390    /// tail expression in a Void-returning function.
4391    #[test]
4392    fn void_effect_op_tail_not_wrapped_in_return() {
4393        let effect_decl = node(
4394            1,
4395            NodeKind::EffectDecl {
4396                annotations: vec![],
4397                visibility: Visibility::Public,
4398                name: ident("Logger"),
4399                generic_params: vec![],
4400                components: vec![],
4401                operations: vec![node(
4402                    2,
4403                    NodeKind::FnDecl {
4404                        annotations: vec![],
4405                        visibility: Visibility::Public,
4406                        is_async: false,
4407                        name: ident("log"),
4408                        generic_params: vec![],
4409                        params: vec![typed_param_node(3, "msg", "String")],
4410                        return_type: Some(Box::new(type_named_node(4, "Void"))),
4411                        effect_clause: vec![],
4412                        where_clause: vec![],
4413                        body: Box::new(block(5, vec![], None)),
4414                    },
4415                )],
4416            },
4417        );
4418        let log_call = node(
4419            10,
4420            NodeKind::Call {
4421                callee: Box::new(id_node(11, "log")),
4422                args: vec![bock_air::AirArg {
4423                    label: None,
4424                    value: str_lit(12, "hello"),
4425                }],
4426                type_args: vec![],
4427            },
4428        );
4429        let caller = node(
4430            20,
4431            NodeKind::FnDecl {
4432                annotations: vec![],
4433                visibility: Visibility::Public,
4434                is_async: false,
4435                name: ident("do_log"),
4436                generic_params: vec![],
4437                params: vec![],
4438                return_type: Some(Box::new(type_named_node(21, "Void"))),
4439                effect_clause: vec![type_path(&["Logger"])],
4440                where_clause: vec![],
4441                body: Box::new(block(22, vec![], Some(log_call))),
4442            },
4443        );
4444        let out = gen(&module(vec![], vec![effect_decl, caller]));
4445        assert!(
4446            out.contains("logger.Log("),
4447            "effect op should be rewritten as handler.Method, got: {out}"
4448        );
4449        assert!(
4450            !out.contains("return logger.Log("),
4451            "Void effect op in Void fn should NOT be preceded by `return`, got: {out}"
4452        );
4453    }
4454}