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::{
20    AIRNode, AirInterpolationPart, EnumVariantPayload, NodeKind, ResultVariant, Visitor,
21};
22use bock_ast::{AssignOp, BinOp, Literal, TypeExpr, UnaryOp, Visibility};
23use bock_types::AIRModule;
24
25use crate::error::CodegenError;
26use crate::generator::{CodeGenerator, GeneratedCode, OutputFile, SourceMap};
27use crate::profile::TargetProfile;
28
29/// Collects the value-identifier names *referenced* anywhere in a subtree.
30///
31/// Used by the Go emitter's unused-binding guard: Go rejects a `let`-bound local
32/// that is never read (`declared and not used`), which Bock permits (a binding
33/// kept only for its side effect, or shadowed later). After emitting such a
34/// binding we append `_ = name` iff the name is not referenced in the rest of the
35/// block — this collector computes that reference set.
36///
37/// Every [`NodeKind::Identifier`] is counted as a reference. Binding *patterns*
38/// (`let x = …`'s `x`) are not identifiers, so they are correctly excluded — only
39/// genuine *uses* land here. Conservative by construction: a name that appears in
40/// any form (a nested closure, a struct-field value, an interpolation) is seen, so
41/// the guard never silences a binding that is actually read.
42struct IdentUseCollector {
43    used: HashSet<String>,
44}
45
46impl Visitor for IdentUseCollector {
47    fn visit_node(&mut self, node: &AIRNode) {
48        if let NodeKind::Identifier { name } = &node.kind {
49            self.used.insert(name.name.clone());
50        }
51        bock_air::visitor::walk_node(self, node);
52    }
53}
54
55/// The set of value-identifier names referenced anywhere in `node` (see
56/// [`IdentUseCollector`]).
57fn collect_used_idents(node: &AIRNode) -> HashSet<String> {
58    let mut c = IdentUseCollector {
59        used: HashSet::new(),
60    };
61    c.visit_node(node);
62    c.used
63}
64
65/// Conservative module scan for `Channel` / `spawn` references.
66fn go_module_uses_concurrency(items: &[AIRNode]) -> bool {
67    items.iter().any(|n| {
68        let s = format!("{n:?}");
69        s.contains("\"Channel\"") || s.contains("\"spawn\"")
70    })
71}
72
73/// Whether a Go loop needs a label so a statement-arm `match`'s `break`/
74/// `continue` can target the loop instead of the inner `switch`.
75///
76/// A label is only required when the jumping `match` lowers to a Go `switch`
77/// (where a bare `break` would exit the switch, not the loop). An `Optional`
78/// `match` lowers to an `if __opt.tag == "Some" { ... } else { ... }` chain
79/// instead — a bare `break`/`continue` there already targets the enclosing
80/// `for`, so labelling it produces a *defined-and-not-used* label that Go
81/// rejects. This refines the shared [`crate::generator::loop_needs_break_label`]
82/// for Go's lowering: it returns true only when a non-Optional statement-arm
83/// `match` with a `break`/`continue` is present (not nested under another loop).
84fn go_loop_needs_label(body: &AIRNode) -> bool {
85    /// Does `node` perform a loop `break`/`continue` reachable from a match arm
86    /// without crossing into a nested loop or function?
87    fn arm_has_jump(node: &AIRNode) -> bool {
88        match &node.kind {
89            NodeKind::Break { .. } | NodeKind::Continue => true,
90            NodeKind::For { .. }
91            | NodeKind::While { .. }
92            | NodeKind::Loop { .. }
93            | NodeKind::FnDecl { .. }
94            | NodeKind::Lambda { .. } => false,
95            NodeKind::Block { stmts, tail } => {
96                stmts.iter().any(arm_has_jump) || tail.as_deref().is_some_and(arm_has_jump)
97            }
98            NodeKind::If {
99                then_block,
100                else_block,
101                ..
102            } => arm_has_jump(then_block) || else_block.as_deref().is_some_and(arm_has_jump),
103            NodeKind::Match { arms, .. } => arms
104                .iter()
105                .any(|a| matches!(&a.kind, NodeKind::MatchArm { body, .. } if arm_has_jump(body))),
106            NodeKind::Guard { else_block, .. } => arm_has_jump(else_block),
107            _ => false,
108        }
109    }
110    /// Find a *switch*-lowered (non-Optional) statement-arm match that jumps the
111    /// loop, not crossing into a nested loop or function.
112    fn find(node: &AIRNode) -> bool {
113        match &node.kind {
114            NodeKind::For { .. }
115            | NodeKind::While { .. }
116            | NodeKind::Loop { .. }
117            | NodeKind::FnDecl { .. }
118            | NodeKind::Lambda { .. } => false,
119            NodeKind::Match { arms, .. } => {
120                // Optional/Result matches lower to if/else where bare
121                // break/continue already target the loop — no label needed for
122                // *this* match.
123                let this_needs_label = !go_match_is_optional(arms)
124                    && !go_match_is_result(arms)
125                    && crate::generator::match_has_statement_arm(arms)
126                    && arms.iter().any(|a| {
127                        matches!(&a.kind, NodeKind::MatchArm { body, .. } if arm_has_jump(body))
128                    });
129                // Even a non-jumping (or Optional) match may *contain* a nested
130                // switch-lowered match that jumps the loop, so always recurse
131                // into the arms.
132                this_needs_label
133                    || arms
134                        .iter()
135                        .any(|a| matches!(&a.kind, NodeKind::MatchArm { body, .. } if find(body)))
136            }
137            NodeKind::Block { stmts, tail } => {
138                stmts.iter().any(find) || tail.as_deref().is_some_and(find)
139            }
140            NodeKind::If {
141                then_block,
142                else_block,
143                ..
144            } => find(then_block) || else_block.as_deref().is_some_and(find),
145            NodeKind::Guard { else_block, .. } => find(else_block),
146            _ => false,
147        }
148    }
149    find(body)
150}
151
152/// Decide whether a Go `match` should lower to a *type*-switch
153/// (`switch v := s.(type) { case T: }`) rather than a *value*-switch
154/// (`switch s { case 5: }`).
155///
156/// Constructor and record patterns dispatch on the scrutinee's dynamic type
157/// (enum variants are distinct Go structs), so any such pattern forces a
158/// type-switch. Literal and bind patterns dispatch on value. A match whose
159/// arms are only wildcard/bind patterns defaults to a value-switch.
160fn go_match_is_type_switch(arms: &[AIRNode]) -> bool {
161    arms.iter().any(|arm| {
162        matches!(
163            &arm.kind,
164            NodeKind::MatchArm { pattern, .. }
165                if matches!(
166                    pattern.kind,
167                    NodeKind::ConstructorPat { .. } | NodeKind::RecordPat { .. }
168                )
169        )
170    })
171}
172
173/// True if any arm is a catch-all (`_` or a bind pattern), which lowers to a Go
174/// `default:` case.
175fn go_match_has_default_arm(arms: &[AIRNode]) -> bool {
176    arms.iter().any(|arm| {
177        matches!(
178            &arm.kind,
179            NodeKind::MatchArm { pattern, .. }
180                if matches!(pattern.kind, NodeKind::WildcardPat | NodeKind::BindPat { .. })
181        )
182    })
183}
184
185/// Runtime helpers for Bock concurrency in Go. A Channel is a wrapper
186/// over `chan interface{}` so the generic shape is simple; `spawn`
187/// launches a goroutine whose result is piped through a 1-element
188/// buffered channel (matching the existing Go async-fn wrapper
189/// convention — cf. F.4.3).
190const CONCURRENCY_RUNTIME_GO: &str = "\
191// ── Bock concurrency runtime ──
192type __bockChannel struct {
193\tq chan interface{}
194}
195
196func __bockChannelNew() (*__bockChannel, *__bockChannel) {
197\tc := &__bockChannel{q: make(chan interface{}, 1024)}
198\treturn c, c
199}
200func (c *__bockChannel) send(v interface{}) { c.q <- v }
201func (c *__bockChannel) recv() interface{}  { return <-c.q }
202func (c *__bockChannel) close()              {}
203
204// __bockSpawn launches the passed channel-returning async computation.
205// In practice the Go async-fn lowerer already wraps bodies in goroutines,
206// so this is the identity on a receive channel.
207func __bockSpawn(ch interface{}) interface{} { return ch }
208";
209
210/// Runtime helpers for Bock `Optional[T]` in Go. Go has no sum type, so an
211/// optional is a tagged struct: `tag` is `"Some"` or `"None"`, `v` carries the
212/// payload for `Some`. `__bockSome`/`__bockNone` are the constructors; matches
213/// dispatch on `.tag` and read `.v` for the bound value.
214///
215/// `__bockAsInt64` / `__bockAsFloat64` recover a numeric payload from the
216/// `interface{}` box. Bock's `Int`/`Float` are Go `int64`/`float64`, but a
217/// payload constructed from an *untyped Go constant* — e.g. `Some(10)` →
218/// `__bockSome(10)` — boxes a Go `int` (the default type of an untyped integer
219/// constant), not an `int64`. A hard `.(int64)` assertion on that box panics
220/// (`interface {} is int, not int64`). These helpers widen the common numeric
221/// boxings instead, so a `Some(x)` payload bound for typed use works whether it
222/// came from a literal, a typed variable, or arithmetic.
223const OPTIONAL_RUNTIME_GO: &str = "// ── Bock Optional runtime ──
224type __bockOption struct {
225	tag string
226	v   interface{}
227}
228
229func __bockSome(v interface{}) __bockOption { return __bockOption{tag: \"Some\", v: v} }
230
231var __bockNone = __bockOption{tag: \"None\"}
232";
233
234/// Shared numeric-widening helpers used by both the `Optional` and `Result`
235/// runtimes to recover an `int64`/`float64` payload from the `interface{}` box.
236///
237/// A payload constructed from an *untyped Go constant* — e.g. `Some(10)` /
238/// `Ok(10)` → `__bockSome(10)` / `__bockOk(10)` — boxes a Go `int` (the default
239/// type of an untyped integer constant), not an `int64`. A hard `.(int64)`
240/// assertion on that box panics (`interface {} is int, not int64`). These helpers
241/// widen the common numeric boxings instead. Emitted once if *either* container
242/// runtime is used (its own emit flag), so the two runtimes never redeclare them.
243const NUMERIC_RUNTIME_GO: &str = "// ── Bock numeric payload helpers ──
244func __bockAsInt64(v interface{}) int64 {
245	switch n := v.(type) {
246	case int64:
247		return n
248	case int:
249		return int64(n)
250	case int32:
251		return int64(n)
252	case float64:
253		return int64(n)
254	default:
255		return 0
256	}
257}
258
259func __bockAsFloat64(v interface{}) float64 {
260	switch n := v.(type) {
261	case float64:
262		return n
263	case float32:
264		return float64(n)
265	case int64:
266		return float64(n)
267	case int:
268		return float64(n)
269	default:
270		return 0
271	}
272}
273";
274
275/// Runtime for Bock `Result[T, E]` in Go. Mirrors `OPTIONAL_RUNTIME_GO`: a
276/// tagged struct (`tag` is `"Ok"`/`"Err"`, `v` carries the payload), with
277/// `__bockOk`/`__bockErr` constructors. A `match r { Ok(v) => …; Err(e) => … }`
278/// dispatches on `.tag` and reads `.v` for the bound value — the same tag-switch
279/// the Optional match uses, not the user-enum type-switch (`case Ok:` against an
280/// undefined Go type) the broken codegen produced.
281const RESULT_RUNTIME_GO: &str = "// ── Bock Result runtime ──
282type __bockResult struct {
283	tag string
284	v   interface{}
285}
286
287func __bockOk(v interface{}) __bockResult { return __bockResult{tag: \"Ok\", v: v} }
288
289func __bockErr(v interface{}) __bockResult { return __bockResult{tag: \"Err\", v: v} }
290";
291
292/// Runtime helper for Bock range expressions (`0..n` / `0..=n`) in Go. Go has
293/// no native range *value*, so `for i in 0..n` lowers to
294/// `for _, i := range __bockRange(0, n, false)`; this builds the `[]int64`
295/// slice with half-open (`inclusive=false`) or inclusive (`inclusive=true`)
296/// bounds, matching Python's `range(lo, hi)` / `range(lo, hi + 1)` and Rust's
297/// `lo..hi` / `lo..=hi`. Emitted once into the shared `bock_runtime.go`
298/// (per-module path) or inlined at most once (single-module path), gated on a
299/// ctx flag (mirrors `OPTIONAL_RUNTIME_GO`).
300const RANGE_RUNTIME_GO: &str = "// ── Bock range runtime ──
301func __bockRange(lo int64, hi int64, inclusive bool) []int64 {
302	end := hi
303	if inclusive {
304		end = hi + 1
305	}
306	r := make([]int64, 0)
307	for i := lo; i < end; i++ {
308		r = append(r, i)
309	}
310	return r
311}
312";
313
314/// Integer exponentiation helper for the `**` operator on integer operands.
315/// Go has no `**` and `math.Pow` returns `float64` (losing integer precision and
316/// type), so an `Int ** Int` lowers to a call to this helper, which does
317/// fast exponentiation-by-squaring and stays in `int64`. A negative exponent
318/// yields `0` (an integer power with a negative exponent has no `int64` value;
319/// Bock callers using fractional results use `Float ** Float`, which routes to
320/// `math.Pow`). Gated by [`go_module_uses_int_pow`] so it is emitted only when a
321/// `**` with non-float operands is present.
322const INT_POW_RUNTIME_GO: &str = "// ── Bock integer-power runtime ──
323func __bockIntPow(base int64, exp int64) int64 {
324	if exp < 0 {
325		return 0
326	}
327	result := int64(1)
328	for exp > 0 {
329		if exp&1 == 1 {
330			result *= base
331		}
332		base *= base
333		exp >>= 1
334	}
335	return result
336}
337";
338
339/// True if the module references a `Range` node anywhere (so the range runtime
340/// helper must be emitted). Mirrors [`go_module_uses_optional`]. `RangePat`
341/// (a match-arm range pattern) does not contain the `Range {` substring, so it
342/// is not matched — the helper is only needed for range *values*.
343fn go_module_uses_range(items: &[AIRNode]) -> bool {
344    items.iter().any(|n| format!("{n:?}").contains("Range {"))
345}
346
347/// True if the module contains any `**` (`BinOp::Pow`) operator (so the
348/// integer-power runtime helper must be emitted). The float path lowers to
349/// `math.Pow`; the int path calls [`INT_POW_RUNTIME_GO`]'s `__bockIntPow`. We
350/// emit the helper whenever *any* `**` is present (Go tolerates an unused
351/// package-level func, so a float-only program harmlessly carries it), rather
352/// than re-deriving operand types here. Mirrors [`go_module_uses_range`]'s
353/// structural debug scan: a `BinaryOp { op: Pow` renders that substring.
354fn go_module_uses_int_pow(items: &[AIRNode]) -> bool {
355    items.iter().any(|n| format!("{n:?}").contains("op: Pow"))
356}
357
358/// Runtime helper for the DQ29 `"deep"` equality lane: `==`/`!=` whose operand
359/// (transitively) involves a `List`/`Map`/`Set` — Go has no `==` for slices or
360/// maps ("can only be compared to nil", a compile error). `reflect.DeepEqual`
361/// gives exactly the §18.5 semantics: element-wise for slices, content-based
362/// and ORDER-INDEPENDENT for maps (Bock `Map` and `Set` both lower to Go
363/// maps), recursive through struct fields, and IEEE for floats (`NaN`-holding
364/// values are not deeply equal — the DQ10 caveat). Needs `import "reflect"`
365/// wherever it is emitted. The shallow lanes never route here: Go struct /
366/// interface `==` is already field-wise.
367const DEEP_EQ_RUNTIME_GO: &str = "// ── Bock structural equality runtime ──
368func __bockDeepEq(a any, b any) bool {
369	return reflect.DeepEqual(a, b)
370}
371
372// DQ31 (§18.5 container equality defers to element conformance): the
373// custom-element deep-equality lane. A container whose element tree carries an
374// explicit `impl Equatable` (the `\"deep_custom\"` lane) must compare elements
375// — and match Map keys / Set members — through the element's `Eq` method, NOT
376// `reflect.DeepEqual` (which is field-wise and would silently ignore the
377// custom equality, diverging from the other targets). Falls back to
378// `reflect.DeepEqual` for any value lacking a custom `Eq`, so all-structural
379// sub-trees behave identically to the `\"deep\"` lane.
380func __bockEqCustom(a any, b any) bool {
381	// Optional/Result runtime wrappers carry their payload in an unexported
382	// `v interface{}` field, so the generic `reflect.Struct` arm below cannot
383	// reach it (`CanInterface()` is false) and bails to `reflect.DeepEqual`,
384	// which would IGNORE a custom `impl Equatable` on the payload (DQ31). Match
385	// the variant tag, then recurse on the payload through `__bockEqCustom` so a
386	// custom-`Eq` element inside the wrapper defers to its `Eq`.
387	if oa, ok := a.(__bockOption); ok {
388		ob, ok := b.(__bockOption)
389		if !ok || oa.tag != ob.tag {
390			return false
391		}
392		if oa.tag == \"None\" {
393			return true
394		}
395		return __bockEqCustom(oa.v, ob.v)
396	}
397	if ra, ok := a.(__bockResult); ok {
398		rb, ok := b.(__bockResult)
399		if !ok || ra.tag != rb.tag {
400			return false
401		}
402		return __bockEqCustom(ra.v, rb.v)
403	}
404	va := reflect.ValueOf(a)
405	vb := reflect.ValueOf(b)
406	// A value with a custom `Eq(Self) bool` method defines its own equality.
407	if eq := va.MethodByName(\"Eq\"); eq.IsValid() && va.Type() == vb.Type() {
408		mt := eq.Type()
409		if mt.NumIn() == 1 && mt.NumOut() == 1 && mt.Out(0).Kind() == reflect.Bool {
410			out := eq.Call([]reflect.Value{vb})
411			return out[0].Bool()
412		}
413	}
414	if !va.IsValid() || !vb.IsValid() || va.Kind() != vb.Kind() {
415		return reflect.DeepEqual(a, b)
416	}
417	switch va.Kind() {
418	case reflect.Slice, reflect.Array:
419		if va.Len() != vb.Len() {
420			return false
421		}
422		for i := 0; i < va.Len(); i++ {
423			if !__bockEqCustom(va.Index(i).Interface(), vb.Index(i).Interface()) {
424				return false
425			}
426		}
427		return true
428	case reflect.Map:
429		if va.Len() != vb.Len() {
430			return false
431		}
432		// Order-independent: every (k, v) in a must match some (bk, bv) in b
433		// under custom element equality (the key's `Eq` governs key-matching,
434		// the value's `Eq` the value comparison). A Set lowers to a Go
435		// map[T]struct{}, so membership is the same scan with a trivial value.
436		bKeys := vb.MapKeys()
437		for _, ka := range va.MapKeys() {
438			found := false
439			for _, kb := range bKeys {
440				if __bockEqCustom(ka.Interface(), kb.Interface()) {
441					if __bockEqCustom(va.MapIndex(ka).Interface(), vb.MapIndex(kb).Interface()) {
442						found = true
443						break
444					}
445				}
446			}
447			if !found {
448				return false
449			}
450		}
451		return true
452	case reflect.Ptr, reflect.Interface:
453		if va.IsNil() || vb.IsNil() {
454			return va.IsNil() == vb.IsNil()
455		}
456		return __bockEqCustom(va.Elem().Interface(), vb.Elem().Interface())
457	case reflect.Struct:
458		// Records and tuples (struct{ Field0 …; Field1 … }): recurse field-wise
459		// so a custom-`Eq` element nested in a struct still defers to its `Eq`.
460		// Codegen emits exported (PascalCased / `Field0`-style) fields, all
461		// reachable through reflection. If any field is unexported (a runtime
462		// wrapper this helper does not special-case above), fall back to
463		// `reflect.DeepEqual` for the whole value rather than ignore it.
464		for i := 0; i < va.NumField(); i++ {
465			if !va.Field(i).CanInterface() {
466				return reflect.DeepEqual(a, b)
467			}
468			if !__bockEqCustom(va.Field(i).Interface(), vb.Field(i).Interface()) {
469				return false
470			}
471		}
472		return true
473	default:
474		return reflect.DeepEqual(a, b)
475	}
476}
477";
478
479/// True if the module contains a `"deep"`- or `"deep_custom"`-lane equality (so
480/// [`DEEP_EQ_RUNTIME_GO`] must be emitted and `\"reflect\"` imported). Mirrors
481/// [`go_module_uses_range`]'s structural debug scan over the checker's
482/// `user_eq` metadata stamp. The `"deep_custom"` lane (DQ31) routes through the
483/// same runtime block, which defines both `__bockDeepEq` and `__bockEqCustom`.
484fn go_module_uses_deep_eq(items: &[AIRNode]) -> bool {
485    items.iter().any(|n| {
486        let dbg = format!("{n:?}");
487        dbg.contains("\"user_eq\": String(\"deep\")")
488            || dbg.contains("\"user_eq\": String(\"deep_custom\")")
489    })
490}
491
492/// True if the module references `Optional`, `Some`, or `None` anywhere — or
493/// calls `pop`, whose DQ30 lowering builds the tagged `__bockOption` runtime
494/// (`__bockSome(v)` / `__bockNone`) — so the Optional runtime prelude must be
495/// emitted. A cheap structural scan over the debug rendering, mirroring
496/// `go_module_uses_concurrency`. Over-matching (a user method named `pop`)
497/// only emits the unused runtime struct, which compiles fine.
498fn go_module_uses_optional(items: &[AIRNode]) -> bool {
499    items.iter().any(|n| {
500        let s = format!("{n:?}");
501        s.contains("\"Optional\"")
502            || s.contains("TypeOptional")
503            || s.contains("\"Some\"")
504            || s.contains("\"None\"")
505            || s.contains("\"pop\"")
506    })
507}
508
509/// True if the module references `Result`, `Ok`, or `Err` anywhere (so the
510/// `Result` runtime prelude must be emitted). Mirrors [`go_module_uses_optional`].
511fn go_module_uses_result(items: &[AIRNode]) -> bool {
512    items.iter().any(|n| {
513        let s = format!("{n:?}");
514        s.contains("\"Result\"")
515            || s.contains("ResultConstruct")
516            || s.contains("\"Ok\"")
517            || s.contains("\"Err\"")
518    })
519}
520
521/// The prelude `Ordering` runtime: a small enum type with the three variants as
522/// package-level constants, plus a generic `compare` helper the primitive bridge
523/// calls. Mirrors `OPTIONAL_RUNTIME_GO` — when the `core.compare` enum decl is
524/// not among the reached modules, `Ordering`/`Less`/`Equal`/`Greater` and
525/// `(x).compare(y)` need this self-contained representation. A value-switch
526/// `case Less:` (the existing Go match lowering for these arms) matches a
527/// `__bockOrdering` constant directly.
528const ORDERING_RUNTIME_GO: &str = "// ── Bock Ordering runtime ──
529type __bockOrdering int
530
531const (
532	Less __bockOrdering = iota - 1
533	Equal
534	Greater
535)
536
537func __bockCompare[T int64 | float64 | string | rune | int | uint64 | float32](a, b T) __bockOrdering {
538	if a < b {
539		return Less
540	}
541	if a == b {
542		return Equal
543	}
544	return Greater
545}
546";
547
548/// Runtime for a `${expr}` interpolation part: render a value whose Bock type
549/// has a `Displayable` impl through its `ToString` method (the user
550/// `to_string` lowers to a `ToString() string` value-receiver method on Go)
551/// rather than the struct default (`fmt.Sprintf("%v", p)` → `{3 7}`). The
552/// `__bockStringer` interface is satisfied by exactly those types; every other
553/// value falls through to `%v`. Gated by [`go_module_uses_str`]; its own `fmt`
554/// import is emitted into the runtime file. (Q-displayable-interpolation-dispatch.)
555const STR_RUNTIME_GO: &str = "// ── Bock display-string runtime ──
556type __bockStringer interface {
557	ToString() string
558}
559
560func __bockStr(x interface{}) string {
561	if s, ok := x.(__bockStringer); ok {
562		return s.ToString()
563	}
564	return fmt.Sprintf(\"%v\", x)
565}
566";
567
568/// The `__bockOrdered` constraint a `[T: Comparable]` sealed-core bound lowers to
569/// (GAP-C): the ordered primitive type-set, so a generic fn's `a.compare(b)` /
570/// `a > b` can use `<`/`==`/`>`. Self-contained (no `cmp` import), matching
571/// `__bockCompare`'s set. Emitted independently of the rest of the Ordering
572/// runtime: a `[T: Comparable]`-bounded fn (`max_of[T: Comparable]`) needs the
573/// constraint even when the module never references `Ordering`/`compare` (which
574/// is what gates [`ORDERING_RUNTIME_GO`]). Deduped against that block so the type
575/// is never defined twice.
576const ORDERED_CONSTRAINT_GO: &str = "// ── Bock ordered constraint ──
577type __bockOrdered interface {
578	~int64 | ~float64 | ~string | ~rune | ~int | ~uint64 | ~float32
579}
580";
581
582/// True if the module references the prelude `Ordering` enum, any of its
583/// variants, or a `compare` method call (lowered to an `Ordering` runtime
584/// value). Gates emission of [`ORDERING_RUNTIME_GO`], mirroring
585/// [`go_module_uses_optional`].
586fn go_module_uses_ordering(items: &[AIRNode]) -> bool {
587    items.iter().any(|n| {
588        let s = format!("{n:?}");
589        s.contains("\"Ordering\"")
590            || s.contains("\"Less\"")
591            || s.contains("\"Equal\"")
592            || s.contains("\"Greater\"")
593            || s.contains("\"compare\"")
594    })
595}
596
597/// True if any module contains a string interpolation (`${expr}`), so the
598/// [`STR_RUNTIME_GO`] `__bockStr` helper must be emitted into the runtime file.
599/// Mirrors [`go_module_uses_optional`]. (Q-displayable-interpolation-dispatch.)
600fn go_module_uses_str(items: &[AIRNode]) -> bool {
601    items
602        .iter()
603        .any(|n| format!("{n:?}").contains("Interpolation"))
604}
605
606/// True if a `match`\'s arms dispatch on the prelude `Ordering` variants
607/// (`Less`/`Equal`/`Greater`), so the Go backend emits a *value*-switch over the
608/// `__bockOrdering` constants rather than the type-switch it uses for user
609/// enums. Recognised by any constructor pattern whose final segment is an
610/// `Ordering` variant.
611fn go_match_is_ordering(arms: &[AIRNode]) -> bool {
612    arms.iter().any(|arm| {
613        if let NodeKind::MatchArm { pattern, .. } = &arm.kind {
614            if let NodeKind::ConstructorPat { path, .. } = &pattern.kind {
615                return path
616                    .segments
617                    .last()
618                    .and_then(|s| crate::generator::ordering_variant(&s.name))
619                    .is_some();
620            }
621        }
622        false
623    })
624}
625
626/// True if a `match`\'s arms dispatch on the `Optional` constructors
627/// `Some`/`None` (so the Go backend emits a tag-based switch over
628/// `__bockOption`). Recognised by a constructor pattern whose final path
629/// segment is `Some` or `None`.
630fn go_match_is_optional(arms: &[AIRNode]) -> bool {
631    arms.iter().any(|arm| {
632        if let NodeKind::MatchArm { pattern, .. } = &arm.kind {
633            if let NodeKind::ConstructorPat { path, .. } = &pattern.kind {
634                return path
635                    .segments
636                    .last()
637                    .is_some_and(|seg| matches!(seg.name.as_str(), "Some" | "None"));
638            }
639        }
640        false
641    })
642}
643
644/// True if a `match`'s arms dispatch on the `Result` constructors `Ok`/`Err`
645/// (so the Go backend emits a tag-based switch over `__bockResult`, mirroring
646/// [`go_match_is_optional`]). Without this, an `Ok`/`Err` constructor pattern
647/// would route to the user-enum type-switch (`case Ok:` against an undefined Go
648/// type) — the defect this fixes.
649fn go_match_is_result(arms: &[AIRNode]) -> bool {
650    arms.iter().any(|arm| {
651        if let NodeKind::MatchArm { pattern, .. } = &arm.kind {
652            if let NodeKind::ConstructorPat { path, .. } = &pattern.kind {
653                return path
654                    .segments
655                    .last()
656                    .is_some_and(|seg| matches!(seg.name.as_str(), "Ok" | "Err"));
657            }
658        }
659        false
660    })
661}
662
663/// Go code generator implementing the `CodeGenerator` trait.
664#[derive(Debug)]
665pub struct GoGenerator {
666    profile: TargetProfile,
667}
668
669impl GoGenerator {
670    /// Creates a new Go code generator.
671    #[must_use]
672    pub fn new() -> Self {
673        Self {
674            profile: TargetProfile::go(),
675        }
676    }
677}
678
679impl Default for GoGenerator {
680    fn default() -> Self {
681        Self::new()
682    }
683}
684
685impl CodeGenerator for GoGenerator {
686    fn target(&self) -> &TargetProfile {
687        &self.profile
688    }
689
690    fn generate_module(&self, module: &AIRModule) -> Result<GeneratedCode, CodegenError> {
691        // Shared pre-pass: hoist value-position diverging control flow (see
692        // `hoist_value_cf`) into declare-then-assign temp blocks.
693        let module =
694            &crate::generator::hoist_value_cf(crate::generator::lower_blanket_into(module.clone()));
695        let mut ctx = GoEmitCtx::new();
696        ctx.enum_variants =
697            crate::generator::collect_enum_variants(&[(module, std::path::Path::new(""))]);
698        ctx.generic_decls =
699            crate::generator::collect_generic_decls(&[(module, std::path::Path::new(""))]);
700        ctx.collect_record_param_fields(module);
701        ctx.collect_async_fns(module);
702        ctx.collect_methods(module);
703        ctx.collect_type_aliases(module);
704        ctx.collect_optional_returns(module);
705        ctx.collect_method_optional_returns(module);
706        // `trait_decls` must precede `collect_fn_and_type_names` so the latter can
707        // record which generic fns carry a *sealed-core* bound lowered to a Go
708        // built-in constraint (GAP-C — `fn_sealed_bound`).
709        ctx.trait_decls =
710            crate::generator::collect_trait_decls(&[(module, std::path::Path::new(""))]);
711        ctx.const_names =
712            crate::generator::collect_const_names(&[(module, std::path::Path::new(""))]);
713        ctx.collect_fn_and_type_names(module);
714        ctx.derive_self_param_traits();
715        ctx.emit_node(module)?;
716        let content = ctx.finish();
717        let source_map = SourceMap {
718            generated_file: String::new(),
719            ..Default::default()
720        };
721        Ok(GeneratedCode {
722            files: vec![OutputFile {
723                path: PathBuf::new(),
724                content,
725                source_map: Some(source_map),
726            }],
727        })
728    }
729
730    /// Emit a per-module **native Go package tree** (spec §20.6.1; DQ19
731    /// resolved): each module the entry program reaches through a real `use` is
732    /// emitted to its **own** `.go` file under `build/go/`, all in one
733    /// `package main`.
734    ///
735    /// ## Package model (flat, single `package main`)
736    ///
737    /// Go requires exactly one package per directory, and same-package symbols
738    /// are visible across files **without** any import. So the cleanest model
739    /// that is genuinely per-file and runs via `go run .` keeps every emitted
740    /// file in `build/go/` as `package main`: a function/record/enum declared
741    /// in `core.option`'s file is referenced directly from `main`'s file, no
742    /// inter-file import. The flat layout (filenames flatten the dotted module
743    /// path — `module core.option` ⇒ `core.option.go`) avoids the subdirectory
744    /// that would make Go treat a module as a *separate* package. §20.6.1 allows
745    /// "the target ecosystem's conventions," and one package across files is
746    /// Go's. (Project mode — S6 — may refine this toward real subpackages with
747    /// capitalized exports.)
748    ///
749    /// `ImportDecl`s therefore emit nothing (same package). The runtime preludes
750    /// (Optional / Result / numeric / Ordering / concurrency / range) are
751    /// emitted **once** into a shared `bock_runtime.go`; consuming files use the
752    /// runtime symbols directly (same package). The minimal `go.mod` (module
753    /// name + go version) run affordance is emitted by the **scaffolder** in
754    /// project mode (S6a / DV18), not by codegen, so `go run .` resolves the
755    /// package. Go uses a native `func main`, so no entry invocation is appended.
756    fn generate_project(
757        &self,
758        modules: &[(&AIRModule, &std::path::Path)],
759    ) -> Result<GeneratedCode, CodegenError> {
760        // Shared pre-pass: hoist value-position diverging control flow on every
761        // module before registry collection or emission (see `hoist_value_cf`).
762        let hoisted: Vec<(AIRModule, &std::path::Path)> = modules
763            .iter()
764            .map(|(m, p)| {
765                (
766                    crate::generator::hoist_value_cf(crate::generator::lower_blanket_into(
767                        (*m).clone(),
768                    )),
769                    *p,
770                )
771            })
772            .collect();
773        let modules: Vec<(&AIRModule, &std::path::Path)> =
774            hoisted.iter().map(|(m, p)| (m, *p)).collect();
775        let modules = modules.as_slice();
776        // Emit only modules the entry program actually `use`s (plus the entry
777        // itself), dependency-ordered — never the prelude-only stdlib.
778        let reachable = crate::generator::reachable_modules(modules);
779        let modules = reachable.as_slice();
780        if modules.is_empty() {
781            return Ok(GeneratedCode { files: vec![] });
782        }
783
784        let entry_idx = modules
785            .iter()
786            .position(|(m, _)| crate::generator::module_declares_main_fn(m))
787            .unwrap_or(modules.len() - 1);
788
789        // Pre-scan async fns across ALL modules so cross-module calls between
790        // async functions route through the Async-suffix wrappers.
791        let mut global_async_fns: HashSet<String> = HashSet::new();
792        for (module, _) in modules {
793            if let NodeKind::Module { items, .. } = &module.kind {
794                for item in items {
795                    if let NodeKind::FnDecl {
796                        is_async: true,
797                        name,
798                        ..
799                    } = &item.kind
800                    {
801                        global_async_fns.insert(name.name.clone());
802                    }
803                }
804            }
805        }
806
807        // A template ctx carries the program-wide analysis (enum variants,
808        // generics, trait/method/Optional-return metadata) collected across the
809        // whole reachable set so a reference in one file to a symbol declared in
810        // another lowers identically to the bundling path. Each per-module ctx
811        // is forked from it.
812        let mut template = GoEmitCtx::new();
813        template.async_fns = global_async_fns;
814        template.enum_variants = crate::generator::collect_enum_variants(modules);
815        template.generic_decls = crate::generator::collect_generic_decls(modules);
816        template.trait_decls = crate::generator::collect_trait_decls(modules);
817        template.const_names = crate::generator::collect_const_names(modules);
818        template.derive_self_param_traits();
819        // Aliases first across the whole reachable set: a fn in one module may
820        // return a `type` alias declared in another, and the Optional/Result
821        // return scan below must see through it.
822        for (module, _) in modules {
823            template.collect_type_aliases(module);
824        }
825        for (module, _) in modules {
826            template.collect_methods(module);
827            template.collect_optional_returns(module);
828            template.collect_method_optional_returns(module);
829            template.collect_record_param_fields(module);
830            template.collect_fn_and_type_names(module);
831        }
832        // Effect-op resolution needs the whole reachable set: a bare op in one
833        // module may belong to an effect declared in another (§10 + DV13).
834        template.seed_effect_registries(modules);
835
836        let mut files: Vec<OutputFile> = Vec::with_capacity(modules.len() + 2);
837        for (i, (module, source_path)) in modules.iter().enumerate() {
838            let mut ctx = template.fork();
839            ctx.per_module = true;
840            ctx.emit_node(module)?;
841            let (body, needs) = ctx.into_parts();
842
843            // Each per-module file is `package main` with its own per-file
844            // `import (...)` block (Go imports are per-file).
845            let mut content = "package main\n".to_string();
846            content.push_str(&needs.render_block());
847            content.push('\n');
848            content.push_str(&body);
849
850            let rel = if i == entry_idx {
851                std::path::PathBuf::from("main.go")
852            } else {
853                go_module_filename(module, source_path, self.target())
854            };
855            let generated_file = rel
856                .file_name()
857                .and_then(|s| s.to_str())
858                .unwrap_or("")
859                .to_string();
860            files.push(OutputFile {
861                path: rel,
862                content,
863                source_map: Some(SourceMap {
864                    generated_file,
865                    ..Default::default()
866                }),
867            });
868        }
869
870        // Shared runtime file: emit exactly the preludes the whole program uses,
871        // once, in their own `package main` file (same package → visible to all).
872        if let Some(runtime) = self.build_runtime_file(modules, &template) {
873            files.push(OutputFile {
874                path: std::path::PathBuf::from("bock_runtime.go"),
875                content: runtime,
876                source_map: None,
877            });
878        }
879
880        // Manifest emission moved to the project-mode scaffolder (S6a / DV18):
881        // codegen emits only the per-module *source* package in all modes; the
882        // `go.mod` run affordance is emitted by `GoScaffolder` in project mode
883        // only (never under `--source-only`). See `scaffold.rs`.
884
885        Ok(GeneratedCode { files })
886    }
887
888    /// Transpile `@test` functions into a `bock_test.go` file (S7).
889    ///
890    /// `go test` runs `func TestXxx(t *testing.T)` in `package main` (same
891    /// package → the test can call the program's unexported functions). Each
892    /// Bock `@test` becomes one such function, with `expect(...)` assertion
893    /// chains lowered to `if <neg> { t.Errorf(...) }`. `framework` is ignored:
894    /// `go test` (stdlib `testing`) is the universal Go framework (§20.6.2).
895    fn generate_tests(
896        &self,
897        modules: &[(&AIRModule, &std::path::Path)],
898        _framework: &str,
899    ) -> Result<crate::generator::TestArtifacts, CodegenError> {
900        let reachable = crate::generator::reachable_modules(modules);
901        let modules = reachable.as_slice();
902        let tests = crate::generator::collect_test_fns(modules);
903        if tests.is_empty() {
904            return Ok(crate::generator::TestArtifacts::default());
905        }
906
907        // Same program-wide analysis `generate_project` builds, so test bodies
908        // lower references (function casing, enum variants, Optional returns)
909        // identically to the runtime package.
910        let mut global_async_fns: HashSet<String> = HashSet::new();
911        for (module, _) in modules {
912            if let NodeKind::Module { items, .. } = &module.kind {
913                for item in items {
914                    if let NodeKind::FnDecl {
915                        is_async: true,
916                        name,
917                        ..
918                    } = &item.kind
919                    {
920                        global_async_fns.insert(name.name.clone());
921                    }
922                }
923            }
924        }
925        let mut template = GoEmitCtx::new();
926        template.async_fns = global_async_fns;
927        template.enum_variants = crate::generator::collect_enum_variants(modules);
928        template.generic_decls = crate::generator::collect_generic_decls(modules);
929        template.trait_decls = crate::generator::collect_trait_decls(modules);
930        template.const_names = crate::generator::collect_const_names(modules);
931        template.derive_self_param_traits();
932        // Aliases first across the whole reachable set: a fn in one module may
933        // return a `type` alias declared in another, and the Optional/Result
934        // return scan below must see through it.
935        for (module, _) in modules {
936            template.collect_type_aliases(module);
937        }
938        for (module, _) in modules {
939            template.collect_methods(module);
940            template.collect_optional_returns(module);
941            template.collect_method_optional_returns(module);
942            template.collect_record_param_fields(module);
943            template.collect_fn_and_type_names(module);
944        }
945        template.seed_effect_registries(modules);
946
947        let mut ctx = template.fork();
948        ctx.per_module = true;
949        for (test_fn, _module_path) in &tests {
950            let NodeKind::FnDecl { name, body, .. } = &test_fn.kind else {
951                continue;
952            };
953            let go_name = go_test_fn_name(&name.name);
954            ctx.buf.push('\n');
955            ctx.writeln(&format!("func {go_name}(t *testing.T) {{"));
956            ctx.indent += 1;
957            ctx.emit_go_test_body(body)?;
958            ctx.indent -= 1;
959            ctx.writeln("}");
960        }
961
962        let (body, needs) = ctx.into_parts();
963        // The test file is `package main`; build its import block (testing plus
964        // whatever the test bodies pull in — fmt/strings/…). gofmt-sorted order.
965        let mut imports: Vec<&str> = vec!["\"testing\""];
966        if needs.fmt {
967            imports.push("\"fmt\"");
968        }
969        if needs.strconv {
970            imports.push("\"strconv\"");
971        }
972        if needs.strings {
973            imports.push("\"strings\"");
974        }
975        if needs.sync {
976            imports.push("\"sync\"");
977        }
978        if needs.time {
979            imports.push("\"time\"");
980        }
981        if needs.utf8 {
982            imports.push("\"unicode/utf8\"");
983        }
984        imports.sort_unstable();
985        let mut content = String::from("package main\n\nimport (\n");
986        for imp in &imports {
987            content.push_str(&format!("\t{imp}\n"));
988        }
989        content.push_str(")\n");
990        content.push_str(&body);
991
992        Ok(crate::generator::TestArtifacts {
993            files: vec![OutputFile {
994                path: std::path::PathBuf::from("bock_test.go"),
995                content,
996                source_map: None,
997            }],
998            entry_append: None,
999        })
1000    }
1001}
1002
1003/// The flat output filename for one non-entry module in the per-module Go
1004/// package: the declared dotted module-path kept verbatim (`module core.option`
1005/// ⇒ `core.option.go`), so every emitted file lives directly in `build/go/`
1006/// (one package per directory — no subdirectory, which Go would treat as a
1007/// separate package). A module with no declared path falls back to its
1008/// source-mirrored file name.
1009///
1010/// The dots are **kept** (not flattened to `_`) deliberately: Go reserves the
1011/// `_test.go` filename suffix for test files (excluded from a normal `go build`
1012/// / `go run .`), so `module core.test` flattened to `core_test.go` would
1013/// silently vanish from the build. `core.test.go` does not match `_test.go` and
1014/// compiles as an ordinary package file. (Go also reserves `_GOOS.go` /
1015/// `_GOARCH.go` suffixes, which the dot form likewise avoids.)
1016fn go_module_filename(
1017    module: &AIRModule,
1018    source_path: &std::path::Path,
1019    target: &TargetProfile,
1020) -> std::path::PathBuf {
1021    match crate::generator::module_path_string(module) {
1022        Some(path) if !path.is_empty() => std::path::PathBuf::from(format!("{path}.go")),
1023        _ => crate::generator::derive_output_path(source_path, target)
1024            .file_name()
1025            .map(std::path::PathBuf::from)
1026            .unwrap_or_else(|| std::path::PathBuf::from("module.go")),
1027    }
1028}
1029
1030impl GoGenerator {
1031    /// Build the shared `bock_runtime.go` for the per-module path: `package main`
1032    /// plus exactly the runtime preludes any reached module references, emitted
1033    /// once (a duplicate `type __bockOption` / `__bockChannel` across files would
1034    /// not compile). Returns `None` when no prelude is needed.
1035    ///
1036    /// The selection mirrors the bundling path's per-prelude gating:
1037    /// numeric helpers are emitted when either container runtime is present
1038    /// (both use them); the bespoke int-`Ordering` runtime is emitted only when
1039    /// the real `core.compare.Ordering` enum is NOT reachable (otherwise that
1040    /// user enum is authoritative and the int runtime would be dead + shadow it).
1041    fn build_runtime_file(
1042        &self,
1043        modules: &[(&AIRModule, &std::path::Path)],
1044        template: &GoEmitCtx,
1045    ) -> Option<String> {
1046        let mut uses_concurrency = false;
1047        let mut uses_optional = false;
1048        let mut uses_result = false;
1049        let mut uses_ordering = false;
1050        let mut uses_range = false;
1051        let mut uses_int_pow = false;
1052        let mut uses_deep_eq = false;
1053        let mut uses_str = false;
1054        for (module, _) in modules {
1055            if let NodeKind::Module { items, .. } = &module.kind {
1056                uses_concurrency |= go_module_uses_concurrency(items);
1057                uses_optional |= go_module_uses_optional(items);
1058                uses_result |= go_module_uses_result(items);
1059                uses_ordering |= go_module_uses_ordering(items);
1060                uses_range |= go_module_uses_range(items);
1061                uses_int_pow |= go_module_uses_int_pow(items);
1062                uses_deep_eq |= go_module_uses_deep_eq(items);
1063                uses_str |= go_module_uses_str(items);
1064            }
1065        }
1066        // The real `core.compare.Ordering` enum is authoritative when reachable
1067        // (its `Less` is a registered user variant in the shared registry).
1068        let ordering_enum_reachable = template
1069            .enum_variants
1070            .get("Less")
1071            .is_some_and(|info| info.enum_name == "Ordering");
1072
1073        let emit_ordering = uses_ordering && !ordering_enum_reachable;
1074        // A `[T: Comparable]`-bounded generic fn lowers `T` to `T __bockOrdered`
1075        // (GAP-C). That constraint type must be defined even when the program
1076        // never references `Ordering`/`compare` (which gates the rest of the
1077        // Ordering runtime). `fn_sealed_bound` is populated on the template's
1078        // program-wide pre-scan.
1079        let emit_ordered_constraint = !template.fn_sealed_bound.is_empty();
1080        if !(uses_concurrency
1081            || uses_optional
1082            || uses_result
1083            || uses_range
1084            || uses_int_pow
1085            || uses_deep_eq
1086            || uses_str
1087            || emit_ordering
1088            || emit_ordered_constraint)
1089        {
1090            return None;
1091        }
1092
1093        let mut content = String::from("package main\n\n");
1094        // `__bockDeepEq` is the only runtime helper with a stdlib dependency;
1095        // its import lives here (Go imports are per-file, and the consuming
1096        // modules only *call* the helper).
1097        if uses_deep_eq {
1098            content.push_str("import \"reflect\"\n\n");
1099        }
1100        if uses_str {
1101            // `__bockStr` calls `fmt.Sprintf`; Go imports are per-file, and the
1102            // runtime file is its own `package main` source.
1103            content.push_str("import \"fmt\"\n\n");
1104        }
1105        if uses_concurrency {
1106            content.push_str(CONCURRENCY_RUNTIME_GO);
1107            content.push('\n');
1108        }
1109        // The deep-eq runtime's `__bockEqCustom` type-asserts the Optional/Result
1110        // wrapper structs to defer their payload to the element's `Eq`
1111        // (Q-py-go-wrapper-structural-eq), so those type definitions must be in
1112        // scope wherever `__bockEqCustom` is. Emit them when `uses_deep_eq` even
1113        // if the program never names `Optional`/`Result` directly; the
1114        // (then-unused) constructors are harmless package-level decls in Go.
1115        if uses_optional || uses_deep_eq {
1116            content.push_str(OPTIONAL_RUNTIME_GO);
1117            content.push('\n');
1118        }
1119        if uses_result || uses_deep_eq {
1120            content.push_str(RESULT_RUNTIME_GO);
1121            content.push('\n');
1122        }
1123        if uses_optional || uses_result {
1124            content.push_str(NUMERIC_RUNTIME_GO);
1125            content.push('\n');
1126        }
1127        if emit_ordering {
1128            content.push_str(ORDERING_RUNTIME_GO);
1129            content.push('\n');
1130        }
1131        // The `__bockOrdered` constraint: needed by a sealed-bound generic fn,
1132        // and (since it was split out of the Ordering block) also whenever the
1133        // Ordering runtime itself is emitted, so a `compare`-using generic still
1134        // resolves it. Emitted once — `emit_ordering` no longer carries it.
1135        if emit_ordered_constraint || emit_ordering {
1136            content.push_str(ORDERED_CONSTRAINT_GO);
1137            content.push('\n');
1138        }
1139        if uses_range {
1140            content.push_str(RANGE_RUNTIME_GO);
1141            content.push('\n');
1142        }
1143        if uses_int_pow {
1144            content.push_str(INT_POW_RUNTIME_GO);
1145            content.push('\n');
1146        }
1147        if uses_deep_eq {
1148            content.push_str(DEEP_EQ_RUNTIME_GO);
1149            content.push('\n');
1150        }
1151        if uses_str {
1152            content.push_str(STR_RUNTIME_GO);
1153            content.push('\n');
1154        }
1155        // Each runtime block is joined with a trailing `\n`, which leaves a blank
1156        // line at EOF; gofmt wants exactly one terminating newline (§20.6.2
1157        // codegen-formatter agreement — the output must be gofmt-clean).
1158        let content = format!("{}\n", content.trim_end());
1159        Some(content)
1160    }
1161}
1162
1163// ─── Emission context ────────────────────────────────────────────────────────
1164
1165/// Internal state for Go emission.
1166///
1167/// `Clone` is derived so the per-module path ([`GoGenerator::generate_project`])
1168/// can pre-scan the whole program's cross-module analysis once into a template
1169/// ctx and [`GoEmitCtx::fork`] it per module file (resetting only the per-file
1170/// emission state). Every field is itself `Clone`.
1171#[derive(Clone)]
1172struct GoEmitCtx {
1173    buf: String,
1174    indent: usize,
1175    /// Track whether we need `"fmt"` import.
1176    needs_fmt_import: bool,
1177    /// Track whether we need `"sync"` import.
1178    needs_sync_import: bool,
1179    /// Track whether we need `"time"` import.
1180    needs_time_import: bool,
1181    /// Track whether we need `"strings"` import (String built-in methods).
1182    needs_strings_import: bool,
1183    /// Track whether we need `"unicode/utf8"` import (`String.len` scalar count).
1184    needs_utf8_import: bool,
1185    /// Track whether we need `"math"` import (numeric `Float` math methods).
1186    needs_math_import: bool,
1187    /// Track whether we need `"unicode"` import (`Char`/`trim_start`/`trim_end`
1188    /// predicates via `unicode.IsSpace`/`IsLetter`/`IsDigit`).
1189    needs_unicode_import: bool,
1190    /// Track whether we need `"strconv"` import (`Int.try_from`/`Float.try_from`
1191    /// string parsing via `strconv.ParseInt`/`strconv.ParseFloat`).
1192    needs_strconv_import: bool,
1193    /// Track whether we need `"reflect"` import (the DQ29 `__bockDeepEq`
1194    /// structural-equality helper, single-module path only — the per-module
1195    /// path imports it inside the shared `bock_runtime.go` instead).
1196    needs_reflect_import: bool,
1197    /// Package name (defaults to "main").
1198    package_name: String,
1199    /// Maps effect operation name → effect type name (e.g., "log" → "Logger").
1200    effect_ops: HashMap<String, String>,
1201    /// Maps effect type name → current handler variable name in scope.
1202    current_handler_vars: HashMap<String, String>,
1203    /// Maps function name → effect type names from its `with` clause.
1204    fn_effects: HashMap<String, Vec<String>>,
1205    /// Maps composite effect name → component effect names.
1206    composite_effects: HashMap<String, Vec<String>>,
1207    /// Names of public (exported) functions — emitted as PascalCase at call sites.
1208    public_fns: HashSet<String>,
1209    /// Names of effect operations that return Void — emitted without a `return` prefix.
1210    void_effect_ops: HashSet<String>,
1211    /// Bock names of top-level async functions. Call-site identifiers in this
1212    /// set are rewritten to `fnNameAsync` so callers receive the channel form
1213    /// of the function (goroutine started, `<-chan T` returned). Without this,
1214    /// `await task()` would try to receive from a `T`, not `chan T`.
1215    async_fns: HashSet<String>,
1216    /// Names of `public` methods (declared in impl/class/trait blocks). Used at
1217    /// desugared method-call sites to pick PascalCase (public) vs camelCase
1218    /// (private) so the call matches the method definition's Go casing.
1219    public_methods: HashSet<String>,
1220    /// `(target type name, method name)` pairs that have an *inherent* (`impl
1221    /// Type { ... }`, no `trait_path`) or *class* method definition. A trait
1222    /// impl (`impl Trait for Type`) whose method merely forwards to the
1223    /// same-named inherent method (`fn render(self) { self.render() }`) is a
1224    /// redundant self-recursive forwarder in Go once the inherent method is
1225    /// exported to satisfy the interface directly — both would emit the same
1226    /// PascalCase Go name on the receiver, and the forwarder body's
1227    /// `self.render()` would resolve back to itself. Such a trait-impl method is
1228    /// skipped when an inherent definition already covers it. Keyed on the
1229    /// PascalCased Go method name (the trait-side casing) so a private inherent
1230    /// method exported via `public_methods` still matches.
1231    inherent_methods: HashSet<(String, String)>,
1232    /// PascalCased names of every record/class field declared in the program.
1233    /// Go forbids a struct having a field and a method with the same name, so a
1234    /// public method whose PascalCased Go name collides with a field name
1235    /// (e.g. `core.error`'s `SimpleError { message }` + `fn message(self)`) is
1236    /// suffixed `Method` by [`Self::go_method_name`] at the declaration (trait
1237    /// interface + receiver) and every call site so they agree.
1238    record_field_names: HashSet<String>,
1239    /// Loop-label stack. In Go, `break` inside a `switch` exits the switch, not
1240    /// an enclosing `for`. When a statement-arm `match` (lowered to a `switch`)
1241    /// contains a `break`/`continue` meant for the loop, the loop is given a
1242    /// label and the jump is emitted as `break <label>` / `continue <label>`.
1243    /// An entry is pushed for every active loop; `Some` once a label has been
1244    /// allocated for it. Only allocated labels are emitted (Go errors on an
1245    /// unused label).
1246    loop_labels: Vec<Option<String>>,
1247    /// When > 0, `break`/`continue` are being emitted inside a `switch` arm and
1248    /// must target the innermost labelled loop rather than the switch.
1249    switch_label_depth: usize,
1250    /// Monotonic counter for unique loop-label names.
1251    loop_label_counter: usize,
1252    /// Monotonic counter for unique guard-let discriminant temp names
1253    /// (`__guard0`, `__guard1`, …), so two `guard (let …)` statements in the same
1254    /// block do not collide.
1255    guard_counter: usize,
1256    /// Monotonic counter for unique `?`-propagation temp names (`__try0`,
1257    /// `__try1`, …). Go has no native `?`; each propagate hoists the operand into
1258    /// a `__tryN` local before its unwrap-or-early-return lowering.
1259    try_counter: usize,
1260    /// Monotonic counter for unique tuple-destructuring-`let` temp names
1261    /// (`__tup0`, `__tup1`, …). Go has no tuple destructuring; a
1262    /// `let (a, b) = expr` hoists `expr` into a `__tupN` struct local and binds
1263    /// each name off its `.Field{i}`, so two such lets in one block do not collide.
1264    let_tuple_counter: usize,
1265    /// Depth of enclosing *expression-position* `loop` IIFEs (`let r = loop { …
1266    /// break <v> }`). Bock's `loop` is a value-producing expression whose
1267    /// `break <v>` yields the loop's value; Go's `for`+`break` carries no value.
1268    /// When this is > 0 the innermost loop body is the IIFE's body, so a
1269    /// `break <v>` lowers to `return <v>` (out of the IIFE), not a value-dropping
1270    /// `break`. Saved/restored around nested statement-position loops, whose
1271    /// `break <v>` is a different (still value-dropping) case.
1272    loop_expr_depth: usize,
1273    /// Maps a function name → the Go element type of its `Optional[T]` return
1274    /// (`int64` for `-> Int?`). Pre-scanned across the module so a `match`
1275    /// whose scrutinee is a call (`match next(it) { Some(x) => ... }`) can
1276    /// type-assert the bound payload. Functions not returning an Optional are
1277    /// absent.
1278    fn_optional_ret_elem: HashMap<String, String>,
1279    /// Maps an in-scope variable name → the Go element type of its `Optional[T]`
1280    /// (e.g. an `o: Int?` parameter or a `let o: Int? = ...` binding maps to
1281    /// `int64`). Lets a `match o { Some(x) => ... }` type-assert `__opt.v` to
1282    /// the concrete element type instead of leaving it `interface{}`. The Go
1283    /// Optional runtime stores the payload as `interface{}`, so without this
1284    /// assertion any typed use of the bound value (`x + 10`) fails Go
1285    /// compilation. Scoped per function body and restored on exit.
1286    var_optional_elem: HashMap<String, String>,
1287    /// Maps an in-scope variable name → its declared type-expression AIR node
1288    /// (an `Optional[Result[(Int, Int), String]]` param maps to that
1289    /// `TypeOptional`/`TypeNamed` node). The single-element `var_*_elem` maps
1290    /// only record the *one-level* peeled Go type, which is not enough to
1291    /// type-assert the payload of a *nested* constructor pattern: a
1292    /// `match v { Some(Ok((a, b))) => … }` must peel Optional → Result → Tuple
1293    /// to assert the boxed `interface{}` payload to its concrete tuple struct
1294    /// (`struct{ Field0 int64; Field1 int64 }`) before `.Field0` reads.
1295    /// Threaded through the pattern-bind/test recursion (peeling Optional on
1296    /// `Some`, Result on `Ok`/`Err`) so a nested tuple pattern lands on the
1297    /// concrete struct type. Scoped per function body and restored on exit.
1298    var_decl_type_node: HashMap<String, AIRNode>,
1299    /// Maps a *method* name → the Go element type of its `Optional[T]` return
1300    /// (`int64` for `fn next(self) -> Int?`). Pre-scanned across every
1301    /// impl/class/trait block so a `match` whose scrutinee is a method call
1302    /// (`match it.next() { Some(x) => ... }`, the shape `for x in <Iterable>`
1303    /// desugars to) can type-assert the bound payload. This is the method-call
1304    /// analogue of [`Self::fn_optional_ret_elem`]. Keyed by method name only
1305    /// (Go codegen sees the AIR, not the checker's per-type `method_types`); if
1306    /// two methods share a name but return different Optional element types, the
1307    /// entry is poisoned (left absent) so the payload falls back to the runtime
1308    /// `interface{}` — conservative, never wrong, only un-type-asserted.
1309    method_optional_ret_elem: HashMap<String, String>,
1310    /// Maps a method name → the concrete generic-record instantiation it returns
1311    /// (`("ListIterator", ["int64"])` for `Bag.iter() -> ListIterator[Int]`),
1312    /// for methods whose declared return type is a concrete generic-record
1313    /// apply (no remaining type params). Lets an *untyped* binding of such a
1314    /// call (`__it := bag.Iter()`, the `for x in <Iterable>` desugar) record the
1315    /// binding's record args ([`Self::var_record_type_args`]) so the
1316    /// subsequent `match __it.next() { Some(x) => ... }` resolves the generic
1317    /// `Optional[T]` payload to the concrete arg (`int64`) — `T` is undefined in
1318    /// the calling fn (`main`). Keyed by method name only; poisoned (left
1319    /// absent) on a name clash with disagreeing args, as
1320    /// [`Self::method_optional_ret_elem`].
1321    method_ret_record_args: HashMap<String, (String, Vec<String>)>,
1322    /// Maps a method name → its declared return type rendered as Go (`stock_value
1323    /// → "float64"`). Lets `infer_go_expr_type` resolve a `recv.method()` call's
1324    /// type so a `.map((p) => p.stock_value())` combinator sizes its result slice
1325    /// as `[]float64` (not the erased `[]interface{}` whose elements a later
1326    /// `fold`'s `acc + v` can't add). Keyed by method name only; poisoned (left
1327    /// absent) on a name clash with disagreeing Go return types — mirrors
1328    /// [`Self::method_optional_ret_elem`]. A return type still naming an in-scope
1329    /// generic param is skipped (it is the generic signature, not concrete).
1330    method_return_go_types: HashMap<String, String>,
1331    /// Maps an in-scope variable name bound to a lambda → that lambda's inferred
1332    /// Go return type (`clip_fn → "[]float64"` for `let clip_fn = (d) => clip(d,
1333    /// ..)`). Lets a compose desugar `normalize >> clip_fn` (lowered to
1334    /// `(__compose_x) => clip_fn(normalize(__compose_x))`) resolve its own return
1335    /// type from the outer local lambda `clip_fn`, so the emitted closure is
1336    /// `func(x []float64) []float64` rather than `func(x []float64) interface{}`
1337    /// (the latter not assignable to a `Fn(List[Float]) -> List[Float]` callee).
1338    /// Function-scoped, restored on body exit alongside `var_go_type`.
1339    var_lambda_ret: HashMap<String, String>,
1340    /// Maps an in-scope variable name → its concrete generic record
1341    /// instantiation `(base record name, concrete Go type-args)` — e.g. a `let
1342    /// c: ListIter[Int]` binding or a `c: Counter[Int]` parameter maps to
1343    /// `("ListIter", ["int64"])`. Used to resolve a method-call scrutinee's
1344    /// `Optional[T]` payload at a CONCRETE call site: `method_optional_ret_elem`
1345    /// stores the *generic* element (`"T"`, the record's type param), undefined
1346    /// in the concrete caller (`main`); this lets `match c.next() { Some(x) =>
1347    /// ... }` assert the payload to the instantiation's arg (`int64`) instead of
1348    /// the bare `T`. Scoped per function/method body and restored on exit.
1349    var_record_type_args: HashMap<String, (String, Vec<String>)>,
1350    /// Maps an in-scope variable name → the Go element type of its `List[T]`
1351    /// (e.g. a `let nums: List[Int] = ...` binding maps to `int64`). The
1352    /// read-only `List` built-ins `get`/`first`/`last` return `Optional[T]`
1353    /// whose payload is the list element; this lets a `match nums.get(i) {
1354    /// Some(x) => ... }` type-assert the `interface{}` payload to the element
1355    /// type, the same way [`Self::var_optional_elem`] handles direct
1356    /// `Optional[T]` bindings. Scoped per function body and restored on exit.
1357    var_list_elem: HashMap<String, String>,
1358    /// Maps an in-scope variable name → `(key_go_type, val_go_type)` of its
1359    /// `Map[K, V]` (e.g. a `let m: Map[String, Int] = ...` binding maps to
1360    /// `("string", "int64")`). The built-in `Map` methods lower to inline
1361    /// `func(__m map[K]V, …) …` closures whose parameter type must match the
1362    /// concretely-typed receiver `map[K]V`; this records the declared key/value
1363    /// Go types so the closure is well-typed (Go does not pass a `map[string]
1364    /// int64` where a `map[interface{}]interface{}` is expected). Scoped per
1365    /// function body and restored on exit (mirrors [`Self::var_list_elem`]).
1366    var_map_kv: HashMap<String, (String, String)>,
1367    /// Maps an in-scope variable name → the Go element type of its `Set[E]`
1368    /// (e.g. a `let s: Set[Int] = ...` binding maps to `int64`). The Set
1369    /// analogue of [`Self::var_map_kv`]: the built-in `Set` methods lower to
1370    /// inline closures over `map[E]struct{}`, so the element type must match the
1371    /// concretely-typed receiver. Scoped per function body and restored on exit.
1372    var_set_elem: HashMap<String, String>,
1373    /// Maps an in-scope variable name → `(ok_go_type, err_go_type)` of its
1374    /// `Result[T, E]` (e.g. an `r: Result[Int, String]` param maps to
1375    /// `("int64", "string")`). The Result analogue of [`Self::var_optional_elem`]:
1376    /// a `match r { Ok(v) => ...; Err(e) => ... }` type-asserts the `interface{}`
1377    /// payload to the concrete Ok/Err type rather than leaving it `interface{}`.
1378    /// Scoped per function body and restored on exit.
1379    var_result_elem: HashMap<String, (String, String)>,
1380    /// Maps a free-function name → `(ok_go_type, err_go_type)` of its
1381    /// `Result[T, E]` return, so a `match parse(s) { Ok(n) => ... }` on a call
1382    /// scrutinee type-asserts the bound payload. The Result analogue of
1383    /// [`Self::fn_optional_ret_elem`]; functions not returning a Result are absent.
1384    fn_result_ret_elem: HashMap<String, (String, String)>,
1385    /// Set once the concurrency runtime prelude has been emitted into `buf` in
1386    /// the single-module self-contained path ([`GoGenerator::generate_module`]),
1387    /// so a module referencing it more than once still inlines it at most once (a
1388    /// duplicate `type __bockChannel` would not compile). The per-module project
1389    /// path emits the runtime once into the shared `bock_runtime.go`.
1390    concurrency_runtime_emitted: bool,
1391    /// Set once the Optional runtime prelude has been emitted into `buf`;
1392    /// deduped exactly as [`Self::concurrency_runtime_emitted`].
1393    optional_runtime_emitted: bool,
1394    /// Set once the `Result` runtime prelude has been emitted; deduped exactly as
1395    /// [`Self::optional_runtime_emitted`].
1396    result_runtime_emitted: bool,
1397    /// Set once the shared numeric-payload helpers ([`NUMERIC_RUNTIME_GO`]) have
1398    /// been emitted. Emitted once if *either* the Optional or `Result` runtime is
1399    /// used, so the two never redeclare `__bockAsInt64`/`__bockAsFloat64`.
1400    numeric_runtime_emitted: bool,
1401    /// Set once the [`ORDERING_RUNTIME_GO`] prelude has been emitted; deduped
1402    /// exactly as [`Self::optional_runtime_emitted`].
1403    ordering_runtime_emitted: bool,
1404    /// Set once [`ORDERED_CONSTRAINT_GO`] (`__bockOrdered`) has been emitted in
1405    /// the single-file inline path; deduped exactly as
1406    /// [`Self::ordering_runtime_emitted`].
1407    ordered_constraint_emitted: bool,
1408    /// Set once the [`RANGE_RUNTIME_GO`] helper has been emitted; deduped exactly
1409    /// as [`Self::optional_runtime_emitted`] (a duplicate `func __bockRange`
1410    /// would not compile).
1411    range_runtime_emitted: bool,
1412    /// Set once the [`INT_POW_RUNTIME_GO`] helper (`__bockIntPow`) has been
1413    /// emitted; deduped exactly as [`Self::range_runtime_emitted`] (a duplicate
1414    /// `func __bockIntPow` would not compile).
1415    int_pow_runtime_emitted: bool,
1416    /// Set once the [`DEEP_EQ_RUNTIME_GO`] helper (`__bockDeepEq`) has been
1417    /// emitted; deduped exactly as [`Self::range_runtime_emitted`].
1418    deep_eq_runtime_emitted: bool,
1419    /// User-enum-variant registry (DV14). Go has no sum type, so a user enum is
1420    /// a sealed interface + per-variant structs named `{enum}{variant}`
1421    /// (e.g. `ShapeCircle`). The registry lets a construction emit the variant
1422    /// struct literal and a `match` emit a *type-switch* (`switch __v :=
1423    /// s.(type) { case ShapeCircle: … }`) with field extraction, rather than the
1424    /// broken value-switch on the unqualified variant name. Built-in
1425    /// Optional/Result pre-seeds are filtered out (Optional has its own
1426    /// `__bockOption` runtime). Pre-scanned across the reached modules.
1427    enum_variants: crate::generator::EnumVariantRegistry,
1428    /// Type-alias registry: alias name → its underlying type-expression AIR node
1429    /// (`type ParseResult = Result[MarkdownNode, ParseError]` →
1430    /// `ParseResult → TypeNamed(Result[...])`). Go has no transparent alias to a
1431    /// *runtime* type the way Bock does: a function returning the alias
1432    /// `ParseResult` must lower to the `__bockResult` runtime struct (so a `match`
1433    /// on its value dispatches on `.tag`), and `result_elem_go_types` /
1434    /// `collect_optional_returns` must see *through* the alias to record the
1435    /// Ok/Err payload types. The emitter resolves an alias name to its target via
1436    /// this map. Pre-scanned across the reached modules (mirrors
1437    /// [`Self::enum_variants`]).
1438    type_aliases: HashMap<String, AIRNode>,
1439    /// Declared names of module-scope `const`s, pre-scanned across the reachable
1440    /// program. Emitted verbatim at both declaration and use so the two agree —
1441    /// `to_pascal_case` (`FIZZ_NUM` → `FIZZNUM`) at the def and `go_fn_name`
1442    /// (`fizzNUM`) at the use otherwise disagree. `SCREAMING_SNAKE` is a valid,
1443    /// exported Go identifier. See [`crate::generator::collect_const_names`].
1444    const_names: std::collections::HashSet<String>,
1445    /// Generic-type declaration registry: a record/enum/class name → its
1446    /// declared generic params. Lets an `impl Box { ... }` block recover the
1447    /// `[T any]` declared on `record Box[T]` so a Go method receiver emits
1448    /// `func (self *Box[T]) ...` (Go requires the type-param list on the
1449    /// receiver) and a construction emits `Box[int64]{...}`. Pre-scanned across
1450    /// the reached modules (mirrors [`Self::enum_variants`]).
1451    generic_decls: crate::generator::GenericDeclRegistry,
1452    /// Method-level type-parameter lowering registry (DQ28). Go forbids type
1453    /// parameters on methods (`func (b Box[T]) Map[U](..)` is a syntax error),
1454    /// but Bock keeps the surface (`Box[T].map[U]`); the Go backend lowers such a
1455    /// method to a *free function* `func Box_Map[T, U](self Box[T], ..) ..`,
1456    /// keyed `<TypeName>_<MethodGoName>` for collision-free naming (free
1457    /// functions support multiple type params natively — no monomorphization).
1458    /// This map records, per Bock *method name*, the owning Go type name, so a
1459    /// call site `box.map(f)` can be rewritten to `Box_Map(box, f)`. Keyed by
1460    /// method name only (codegen sees the AIR, not the checker's per-type method
1461    /// table); if two distinct types declare a generic method of the same name
1462    /// the entry is *poisoned* (removed) — the call site then falls back to the
1463    /// ordinary method-dispatch form, which is at worst un-lowered, never wrong
1464    /// for the unambiguous types. Pre-scanned across the reached modules.
1465    method_freefn_lowered: HashMap<String, String>,
1466    /// Maps an in-scope variable name → its Go type, used to infer a lambda's
1467    /// return type. Go infers a bare `func(...) interface{}` for every lambda;
1468    /// when such a closure is passed to a typed `func(int64) int64` parameter
1469    /// the assignment fails to compile. Tracking param/binding Go types lets the
1470    /// lambda emitter recover a concrete return type structurally from the body.
1471    /// Scoped per function/lambda body and restored on exit.
1472    var_go_type: HashMap<String, String>,
1473    /// Stack of value names already declared in each *Go block scope* currently
1474    /// open, innermost frame last. Used to lower a shadowing `let` correctly:
1475    /// Bock permits re-binding the same name in one block (the immutable-update
1476    /// idiom, `let acc = …; let acc = f(acc)`), but Go's `:=` rejects a
1477    /// re-declaration with no new variable on the left side. When a `let`'s name
1478    /// is already in the *innermost* frame, the binding lowers to a plain
1479    /// assignment (`acc = …`) instead of a fresh declaration (`acc := …` /
1480    /// `var acc T = …`). A new frame is pushed on entry to each Go block body
1481    /// (see [`Self::emit_block_body_inner`]) and popped on exit; the function's
1482    /// body frame is pre-seeded with the parameter names (via
1483    /// [`Self::pending_scope_seed`]) so a `let` shadowing a parameter (same Go
1484    /// scope) also reassigns. A name first declared in a *nested* block is not in
1485    /// an outer frame, so it stays a `:=` declaration — Go permits that legal
1486    /// inner-scope shadow.
1487    go_declared_scopes: Vec<HashSet<String>>,
1488    /// Names to merge into the next Go block frame pushed by
1489    /// [`Self::emit_block_body_inner`]. Set at function/method entry to the
1490    /// parameter names so the function body's frame (which shares the function's
1491    /// single Go scope — there is no extra brace for the body) treats a `let`
1492    /// shadowing a parameter as a reassignment. Consumed (taken) by the next
1493    /// frame push so it never leaks into a nested block.
1494    pending_scope_seed: Option<Vec<String>>,
1495    /// Maps a declare-only temp name (from the shared value-CF hoist) → the Go
1496    /// type inferred for its `var __bock_cf_N T` declaration. Go has no
1497    /// deferred-init `var x` (it needs a type), so a block emitter pre-scans each
1498    /// declare-only `let` paired with its following relocated control-flow
1499    /// statement, infers the result type structurally, and records it here for
1500    /// the `LetBinding` emitter. See [`Self::seed_decl_only_types`].
1501    decl_only_types: HashMap<String, String>,
1502    /// Maps a generic record's name → for each generic param (in declaration
1503    /// order) the field name whose declared type is exactly that param. Lets a
1504    /// construction `Box { value: 42 }` emit the explicit instantiation
1505    /// `Box[int64]{...}` Go requires (Go does *not* infer struct type args from
1506    /// composite-literal field values). `None` for a param not directly named by
1507    /// any field's type (then the arg falls back to `any`). Pre-scanned.
1508    record_param_fields: HashMap<String, Vec<Option<String>>>,
1509    /// Maps a record name → (field name → the Go element type of that field's
1510    /// `List[...]` declared type). Lets a built-in list method on a `self.field`
1511    /// receiver inside a (generic) method type its inline closure's `[]<elem>`
1512    /// parameter correctly: inside `fn next(self)` of `record ListIter[T] { xs:
1513    /// List[T] }`, `self.xs.get(i)` must take `[]T` (T is in scope on the
1514    /// receiver), not `[]interface{}` (which a `[]T` argument does not satisfy).
1515    /// Only `List`-typed fields are recorded. Pre-scanned across the reached modules.
1516    record_field_list_elem: HashMap<String, HashMap<String, String>>,
1517    /// Maps a record name → (field name → the Go `(key, value)` types of that
1518    /// field's `Map[K, V]` declared type). The `Map` analogue of
1519    /// [`Self::record_field_list_elem`]: lets a built-in map method on a
1520    /// `record.field` receiver (`report.by_category.get(k)` for `by_category:
1521    /// Map[String, Float]`) type its inline closure's `map[K]V`/`K`/`V`
1522    /// parameters from the field's declared key/value types rather than the
1523    /// erased `map[interface{}]interface{}` Go rejects against the concrete
1524    /// struct field. Only `Map`-typed fields are recorded. Pre-scanned.
1525    record_field_map_kv: HashMap<String, HashMap<String, (String, String)>>,
1526    /// Maps a record name → (field name → the Go type of that field), for every
1527    /// field of every record. The general scalar analogue of
1528    /// [`Self::record_field_list_elem`]/[`Self::record_field_map_kv`]: lets
1529    /// [`Self::infer_go_expr_type`] resolve a bare `obj.field` access (e.g. a
1530    /// `.map((b) => b.id)` closure body where `b: Block` and `record Block { id:
1531    /// Int }`) to the field's concrete Go type (`int64`), so the result slice is
1532    /// sized `[]int64` rather than the erased `[]interface{}` Go rejects against
1533    /// a declared `[]int64` return. Pre-scanned across the reached modules.
1534    record_field_go_type: HashMap<String, HashMap<String, String>>,
1535    /// Maps a record name → its generic-param names in declaration order
1536    /// (`"SortedSet" → ["T"]`). Lets a construction site substitute a field's
1537    /// declared list-element type (`record SortedSet[T] { items: List[T] }` →
1538    /// elem `T`) with the construct's resolved concrete type args, so an empty
1539    /// `[]` field literal emits `[]Key{}` for `SortedSet[Key]{…}` (or `[]T{}`
1540    /// when the construct is itself generic) rather than the erased
1541    /// `[]interface{}{}` Go rejects against the `[]T` struct field. Pre-scanned.
1542    record_generic_param_names: HashMap<String, Vec<String>>,
1543    /// The base name of the record whose method body is currently being emitted
1544    /// (`"ListIter"` inside `impl ListIter`'s methods), so a `self.field` list
1545    /// receiver resolves through [`Self::record_field_list_elem`]. Set at method
1546    /// entry, restored on exit; `None` outside an impl method body.
1547    current_self_record: Option<String>,
1548    /// Trait-declaration registry. Used at each `impl Trait for Type` site to
1549    /// recover the trait's *default* methods (those carrying a body) so a
1550    /// receiver method is synthesized on the target — the Go interface declares
1551    /// only the signature, so a type relying on an inherited default would
1552    /// otherwise fail to satisfy the interface and have no such method. Pre-
1553    /// scanned across the reached modules (mirrors [`Self::enum_variants`]).
1554    trait_decls: crate::generator::TraitDeclRegistry,
1555    /// Names of all top-level types (records, enums, traits, classes). A public
1556    /// Bock function whose PascalCased Go name collides with one of these (e.g.
1557    /// `public fn key` → `Key`, colliding with `record Key`) is renamed via
1558    /// [`Self::go_fn_name`] — Go has one namespace for types and functions, and
1559    /// PascalCasing erases the `key`/`Key` case distinction Bock relies on.
1560    type_names: HashSet<String>,
1561    /// When `Some(target)`, a `Self` type (`TypeSelf`) renders as `target`
1562    /// rather than the `/* Self */` placeholder. Set while emitting a trait-impl
1563    /// method on a concrete target — most relevant for a *synthesized default
1564    /// method* whose source uses `Self` (e.g. `other: Self`), which must become
1565    /// the concrete receiver type so the Go method signature is valid. Cleared
1566    /// everywhere else.
1567    go_self_subst: Option<String>,
1568    /// Trait names whose methods take a `Self`-typed operand (e.g.
1569    /// `Comparable`/`Equatable`, whose `compare`/`eq` take `other: Self`). Such
1570    /// traits are encoded as F-bounded *generic* interfaces in Go (`type
1571    /// Comparable[T any] interface { Compare(T) Ordering }`) and a bound `[T:
1572    /// Comparable]` lowers to `[T Comparable[T]]` — a plain Go interface cannot
1573    /// name the implementing type. Derived from [`Self::trait_decls`].
1574    self_param_traits: HashSet<String>,
1575    /// The Go return type of the function/method whose body is currently being
1576    /// emitted in *return position*. An `if`/`match` in expression position
1577    /// lowers to an IIFE; typing that IIFE with this concrete return type
1578    /// (`func() Ordering { … }`) rather than `func() interface{}` makes its
1579    /// result assignable where the concrete type is required (e.g. a user-enum
1580    /// `Ordering` return — `interface{}` does not satisfy a named interface).
1581    /// `None` outside a typed return body. The match/if IIFE also emits a
1582    /// trailing `panic("unreachable")` instead of `return nil` when typed, since
1583    /// a concrete (non-interface) return type has no `nil`.
1584    current_fn_ret_type: Option<String>,
1585    /// The enclosing function's declared return type *node*, kept when it is a
1586    /// function type (`Fn(Int) -> Int`). A lambda in tail-return position
1587    /// (`fn compose(...) -> Fn(Int) -> Int { (x) => f(g(x)) }`) is otherwise
1588    /// emitted with `interface{}` params and an `interface{}` return — not
1589    /// assignable to the declared `func(int64) int64` return. This lets the
1590    /// return-position emitter type that lambda's params/return from the declared
1591    /// function type. `None` outside a typed return body (or when the return type
1592    /// is not a function type). Saved/restored alongside `current_fn_ret_type`.
1593    current_fn_ret_type_node: Option<AIRNode>,
1594    /// The Go type a value-position expression is being assigned *into*, when
1595    /// known and distinct from the enclosing function's return type. Set around a
1596    /// `let x: T = <value>`'s value emit. An expression-position `match` lowers
1597    /// to an IIFE whose return type must be the *binding*'s declared `T`, not the
1598    /// function's return type (`current_fn_ret_type`) — a `let x: T = match …`
1599    /// where `T` ≠ the fn return otherwise emits `func() <RetType> { … }()` whose
1600    /// result is not assignable to `T`. When set (and not `interface{}`), the
1601    /// match/if IIFE prefers this over `current_fn_ret_type`. `None` outside a
1602    /// typed binding context; consumed (taken) around the value emit so it never
1603    /// leaks to a sibling/outer expression. Additive: when absent the IIFE keeps
1604    /// using `current_fn_ret_type`, preserving the working Optional/Result/enum
1605    /// return-position behavior.
1606    current_expected_type: Option<String>,
1607    /// The concrete Go type-argument suffix (`[int64]`) of the user enum whose
1608    /// `match` is currently being lowered to a type-switch / type-assert chain,
1609    /// or `""` when none / the enum is non-generic. A generic user enum's variant
1610    /// structs carry the enum's params (`type BoxFull[T any] struct{…}`), so a
1611    /// `case BoxFull[int64]:` / `_, ok := __v.(BoxFull[int64])` must spell the
1612    /// concrete instantiation — Go rejects a bare generic type without
1613    /// instantiation. Set from the scrutinee's inferred Go type before a match's
1614    /// arms are emitted and restored afterwards (matches nest). See
1615    /// [`Self::enum_variant_type_arg_suffix`].
1616    current_match_enum_type_args: String,
1617    /// Expected collection element Go types for a collection literal emitted in
1618    /// a *typed context* (a `let x: List[T] = [...]`). A collection literal
1619    /// infers its element type from its elements, but an EMPTY literal (`[]`)
1620    /// or one whose elements infer looser than the declaration cannot — and the
1621    /// `interface{}` fallback then mismatches the declared `[]T`. When set, a
1622    /// `List`/`Set` literal uses `.0` as its element type and a `Map` literal
1623    /// uses `(.0, .1)` as `(key, value)`, so the literal matches the declared
1624    /// container. `None` outside a typed-collection binding context. Consumed
1625    /// (taken) at the literal so it never leaks to a nested/sibling literal.
1626    expected_collection_elem: Option<(String, Option<String>)>,
1627    /// The enclosing function's *return* collection element Go types, when its
1628    /// return type is a `List[T]` / `Set[T]` / `Map[K, V]`. A collection literal
1629    /// in `return` position adopts these so a generic `fn single[T](x: T) ->
1630    /// List[T] { return [x] }` emits `[]T{x}` rather than the `[]interface{}{x}`
1631    /// the bare-literal inference falls back to (which is not assignable to the
1632    /// `[]T` return). Set at fn/method entry from the return type, restored on
1633    /// exit; `None` for a non-collection or absent return type.
1634    current_fn_ret_collection_elem: Option<(String, Option<String>)>,
1635    /// Signatures of top-level generic functions, keyed by fn name: the declared
1636    /// generic-param names and each value param's declared type node. Used at a
1637    /// call site to type an *untyped lambda argument* (`Filter(it, (x) => x >
1638    /// 2)`): the non-lambda arguments bind the fn's type params to concrete Go
1639    /// types, and the lambda's `Fn(T) -> U` parameter type is then specialised
1640    /// (`func(int64) bool`) so the emitted closure's param is `x int64`, not the
1641    /// `interface{}` default an unannotated param falls back to (which both
1642    /// breaks `x > 2` arithmetic and mismatches the `func(int64) bool`
1643    /// parameter). Only generic fns are recorded (a non-generic fn's lambda arg
1644    /// already types correctly). Pre-scanned across the reached modules.
1645    fn_signatures: HashMap<String, GoFnSig>,
1646    /// Names of generic fns whose bound was lowered to a Go built-in constraint
1647    /// from a sealed-core trait (`[T: Comparable]` → `[T __bockOrdered]`, GAP-C).
1648    /// Under such a constraint Go infers `T` from an *untyped* constant arg as the
1649    /// default type (`int`, not `int64`), mismatching an `int64`-typed
1650    /// destination, so the call site must synthesise an explicit type arg
1651    /// (`max2[int64](9, 7)`) even though the signature touches no container. Set
1652    /// during the same pre-scan as [`Self::fn_signatures`].
1653    fn_sealed_bound: std::collections::HashSet<String>,
1654    /// Maps a *non-generic* top-level fn name → its rendered Go return type
1655    /// (`"key" → "Key"`). Lets [`Self::infer_go_expr_type`] type a call to a
1656    /// concrete constructor/helper so a list literal of such calls (`[key(3),
1657    /// key(1)]`) infers the homogeneous element type `Key` and emits `[]Key{…}`
1658    /// — which in turn lets a generic callee taking that slice (`from_list`,
1659    /// `max_of`) infer its element type rather than collapsing to
1660    /// `[]interface{}` / `[any]`. Generic fns live in [`Self::fn_signatures`].
1661    fn_return_go_types: HashMap<String, String>,
1662    /// Maps a *non-generic* top-level fn name → its value params' declared type
1663    /// nodes (the same `Vec<Option<AIRNode>>` shape [`Self::fn_signatures`] holds
1664    /// for generic fns). A non-generic fn taking a concrete `Fn(Todo) -> Bool`
1665    /// parameter — e.g. `count_where(todos, pred)` — must still pin an untyped
1666    /// `(t) => t.done` lambda argument to `func(t Todo) bool`; without this map
1667    /// the lambda erases to `func(t interface{}) bool` and `t.Done` fails (Go has
1668    /// no field on `interface{}`). Pre-scanned alongside `fn_signatures`; consumed
1669    /// at the call site as the lambda-specialisation fallback when the callee is
1670    /// not generic.
1671    fn_param_types: HashMap<String, Vec<Option<AIRNode>>>,
1672    /// The concrete Go parameter types an *untyped lambda argument* should adopt
1673    /// at its current call site, derived from the callee's generic signature
1674    /// specialised by the other arguments ([`Self::fn_signatures`]). A lambda's
1675    /// own params carry no source annotation (`(x) => x > 2`), so without this
1676    /// they default to `interface{}` — which both breaks the body's arithmetic
1677    /// and mismatches the typed `func(int64) bool` callee parameter. Set just
1678    /// before emitting such an argument, consumed (taken) by the lambda emit so
1679    /// it never leaks to a nested lambda. `None` for an ordinarily-typed lambda.
1680    expected_lambda_param_types: Option<Vec<String>>,
1681    /// A forced Go return type for the *next* lambda emitted, consumed (taken) by
1682    /// the `Lambda` arm. A predicate combinator (`filter`/`find`/`any`/`all`)
1683    /// always takes a `Bool`-returning closure, but the body may be a method call
1684    /// (`(p) => p.in_stock()`) or a `match` whose Go return type
1685    /// [`Self::infer_go_expr_type`] cannot recover — leaving the closure typed
1686    /// `func(T) interface{}`, which then fails `if __f(__x)` (`non-boolean
1687    /// condition`). Setting this to `Some("bool")` pins the predicate's return so
1688    /// the closure type-checks. `None` for an ordinarily-inferred lambda.
1689    forced_lambda_ret: Option<String>,
1690    /// True in the **per-module native-package** emission path
1691    /// ([`GoGenerator::generate_project`], the sole real-build path). When set,
1692    /// the `Module` arm does **not** inline the runtime preludes (they are
1693    /// emitted once into the shared `bock_runtime.go` by `generate_project`) —
1694    /// each module is its own `package main` file and same-package symbols are
1695    /// visible without an import. When clear, the module is emitted as a single
1696    /// self-contained file with its runtime preludes inlined — the
1697    /// [`GoGenerator::generate_module`] path used by unit tests.
1698    per_module: bool,
1699}
1700
1701/// The set of Go stdlib packages the emitted body needs imported, gathered as
1702/// it is generated. Rendered into a single deduped `import (...)` block by
1703/// [`GoImportNeeds::render_block`]. Shared by the two emission entry points
1704/// ([`GoEmitCtx::into_parts`] for the per-module path and
1705/// [`GoEmitCtx::finish`] for the single-module path) so the import logic lives
1706/// in one place.
1707#[derive(Default, Clone, Copy)]
1708struct GoImportNeeds {
1709    fmt: bool,
1710    sync: bool,
1711    time: bool,
1712    strings: bool,
1713    utf8: bool,
1714    math: bool,
1715    unicode: bool,
1716    strconv: bool,
1717    reflect: bool,
1718}
1719
1720impl GoImportNeeds {
1721    /// Render the needed packages as a Go `import` clause (`import "x"` for a
1722    /// single package, an `import (...)` block for several), or the empty string
1723    /// when nothing is needed. The order matches `gofmt`'s lexical sort.
1724    fn render_block(self) -> String {
1725        // Packages are pushed in `gofmt`'s lexical sort order.
1726        let mut imports = Vec::new();
1727        if self.fmt {
1728            imports.push("\"fmt\"");
1729        }
1730        if self.math {
1731            imports.push("\"math\"");
1732        }
1733        if self.reflect {
1734            imports.push("\"reflect\"");
1735        }
1736        if self.strconv {
1737            imports.push("\"strconv\"");
1738        }
1739        if self.strings {
1740            imports.push("\"strings\"");
1741        }
1742        if self.sync {
1743            imports.push("\"sync\"");
1744        }
1745        if self.time {
1746            imports.push("\"time\"");
1747        }
1748        if self.unicode {
1749            imports.push("\"unicode\"");
1750        }
1751        if self.utf8 {
1752            imports.push("\"unicode/utf8\"");
1753        }
1754        if imports.is_empty() {
1755            return String::new();
1756        }
1757        if imports.len() == 1 {
1758            return format!("\nimport {}\n", imports[0]);
1759        }
1760        let mut block = String::from("\nimport (\n");
1761        for imp in &imports {
1762            block.push_str(&format!("\t{imp}\n"));
1763        }
1764        block.push_str(")\n");
1765        block
1766    }
1767}
1768
1769/// A recorded generic-function signature ([`GoEmitCtx::fn_signatures`]): the
1770/// declared generic-param names, each value param's declared type node, and the
1771/// return type node. Used to specialise an untyped lambda argument at a call
1772/// site (bind the type params from the non-lambda args, substitute into the
1773/// lambda's `Fn(...)` param type) and to infer a generic call's result type.
1774type GoFnSig = (Vec<String>, Vec<Option<AIRNode>>, Option<AIRNode>);
1775
1776impl GoEmitCtx {
1777    fn new() -> Self {
1778        Self {
1779            buf: String::with_capacity(4096),
1780            indent: 0,
1781            needs_fmt_import: false,
1782            needs_sync_import: false,
1783            needs_time_import: false,
1784            needs_strings_import: false,
1785            needs_utf8_import: false,
1786            needs_math_import: false,
1787            needs_unicode_import: false,
1788            needs_strconv_import: false,
1789            needs_reflect_import: false,
1790            package_name: "main".into(),
1791            effect_ops: HashMap::new(),
1792            current_handler_vars: HashMap::new(),
1793            fn_effects: HashMap::new(),
1794            composite_effects: HashMap::new(),
1795            public_fns: HashSet::new(),
1796            void_effect_ops: HashSet::new(),
1797            async_fns: HashSet::new(),
1798            public_methods: HashSet::new(),
1799            inherent_methods: HashSet::new(),
1800            record_field_names: HashSet::new(),
1801            loop_labels: Vec::new(),
1802            switch_label_depth: 0,
1803            loop_label_counter: 0,
1804            guard_counter: 0,
1805            try_counter: 0,
1806            let_tuple_counter: 0,
1807            loop_expr_depth: 0,
1808            fn_optional_ret_elem: HashMap::new(),
1809            var_optional_elem: HashMap::new(),
1810            var_decl_type_node: HashMap::new(),
1811            method_optional_ret_elem: HashMap::new(),
1812            method_ret_record_args: HashMap::new(),
1813            method_return_go_types: HashMap::new(),
1814            var_lambda_ret: HashMap::new(),
1815            var_record_type_args: HashMap::new(),
1816            var_list_elem: HashMap::new(),
1817            var_map_kv: HashMap::new(),
1818            var_set_elem: HashMap::new(),
1819            var_result_elem: HashMap::new(),
1820            fn_result_ret_elem: HashMap::new(),
1821            concurrency_runtime_emitted: false,
1822            optional_runtime_emitted: false,
1823            result_runtime_emitted: false,
1824            numeric_runtime_emitted: false,
1825            ordering_runtime_emitted: false,
1826            ordered_constraint_emitted: false,
1827            range_runtime_emitted: false,
1828            int_pow_runtime_emitted: false,
1829            deep_eq_runtime_emitted: false,
1830            enum_variants: crate::generator::EnumVariantRegistry::new(),
1831            type_aliases: HashMap::new(),
1832            const_names: std::collections::HashSet::new(),
1833            generic_decls: crate::generator::GenericDeclRegistry::new(),
1834            method_freefn_lowered: HashMap::new(),
1835            var_go_type: HashMap::new(),
1836            go_declared_scopes: Vec::new(),
1837            pending_scope_seed: None,
1838            decl_only_types: HashMap::new(),
1839            record_param_fields: HashMap::new(),
1840            record_field_list_elem: HashMap::new(),
1841            record_field_map_kv: HashMap::new(),
1842            record_field_go_type: HashMap::new(),
1843            record_generic_param_names: HashMap::new(),
1844            current_self_record: None,
1845            trait_decls: crate::generator::TraitDeclRegistry::new(),
1846            type_names: HashSet::new(),
1847            go_self_subst: None,
1848            self_param_traits: HashSet::new(),
1849            current_fn_ret_type: None,
1850            current_fn_ret_type_node: None,
1851            current_expected_type: None,
1852            current_match_enum_type_args: String::new(),
1853            current_fn_ret_collection_elem: None,
1854            fn_signatures: HashMap::new(),
1855            fn_sealed_bound: std::collections::HashSet::new(),
1856            fn_return_go_types: HashMap::new(),
1857            fn_param_types: HashMap::new(),
1858            expected_lambda_param_types: None,
1859            forced_lambda_ret: None,
1860            expected_collection_elem: None,
1861            per_module: false,
1862        }
1863    }
1864
1865    /// Clone the program-wide cross-module *analysis* state into a fresh
1866    /// emission context for one file of the per-module tree, resetting only the
1867    /// per-file emission state (output buffer + indent, the `needs_*` per-file
1868    /// import flags, and the runtime-once flags). The analysis registries
1869    /// (`enum_variants`, `trait_decls`, method/Optional-return metadata, …) are
1870    /// carried so a reference in one file to a symbol declared in another
1871    /// resolves correctly across the per-module tree.
1872    fn fork(&self) -> Self {
1873        let mut c = self.clone();
1874        c.buf = String::with_capacity(4096);
1875        c.indent = 0;
1876        c.needs_fmt_import = false;
1877        c.needs_sync_import = false;
1878        c.needs_time_import = false;
1879        c.needs_strings_import = false;
1880        c.needs_utf8_import = false;
1881        c.needs_math_import = false;
1882        c.needs_unicode_import = false;
1883        c.needs_strconv_import = false;
1884        c.needs_reflect_import = false;
1885        c.concurrency_runtime_emitted = false;
1886        c.optional_runtime_emitted = false;
1887        c.result_runtime_emitted = false;
1888        c.numeric_runtime_emitted = false;
1889        c.ordering_runtime_emitted = false;
1890        c.ordered_constraint_emitted = false;
1891        c.range_runtime_emitted = false;
1892        c.int_pow_runtime_emitted = false;
1893        c.deep_eq_runtime_emitted = false;
1894        c.per_module = false;
1895        c
1896    }
1897
1898    /// Pre-seed the effect registries (`effect_ops`, `composite_effects`,
1899    /// `void_effect_ops`) from every module's top-level `EffectDecl`s. In the
1900    /// per-module path each module is emitted by its own forked context, so a
1901    /// bare op `log(...)` used in `main` whose effect `Log` is declared in
1902    /// another module must be recognised without having emitted the declaring
1903    /// module first (cross-module effects, §10). Mirrors the Python / JS / TS /
1904    /// Rust backends' equivalents.
1905    fn seed_effect_registries(&mut self, modules: &[(&AIRModule, &std::path::Path)]) {
1906        for (module, _) in modules {
1907            let NodeKind::Module { items, .. } = &module.kind else {
1908                continue;
1909            };
1910            for item in items {
1911                let NodeKind::EffectDecl {
1912                    name,
1913                    components,
1914                    operations,
1915                    ..
1916                } = &item.kind
1917                else {
1918                    continue;
1919                };
1920                if !components.is_empty() {
1921                    let comp_names: Vec<String> = components
1922                        .iter()
1923                        .map(|tp| {
1924                            tp.segments
1925                                .last()
1926                                .map_or("effect".to_string(), |s| s.name.clone())
1927                        })
1928                        .collect();
1929                    self.composite_effects.insert(name.name.clone(), comp_names);
1930                    continue;
1931                }
1932                for op in operations {
1933                    if let NodeKind::FnDecl {
1934                        name: op_name,
1935                        return_type,
1936                        ..
1937                    } = &op.kind
1938                    {
1939                        self.effect_ops
1940                            .insert(op_name.name.clone(), name.name.clone());
1941                        if return_type.as_deref().is_some_and(Self::is_void_type) {
1942                            self.void_effect_ops.insert(op_name.name.clone());
1943                        }
1944                    }
1945                }
1946            }
1947        }
1948    }
1949
1950    /// The Go type to use for an expression-position `if`/`match` IIFE return.
1951    ///
1952    /// Prefers the binding's *expected* type ([`Self::current_expected_type`],
1953    /// set around a `let x: T = …` value emit) when known and concrete, so a
1954    /// value-position `let x: T = match …` produces `func() T { … }()` —
1955    /// assignable to `T` even when `T` differs from the enclosing function's
1956    /// return type. An `interface{}` expected type is ignored (it carries no more
1957    /// information than the untyped fallback and would suppress a more specific
1958    /// `current_fn_ret_type`). Falls back to the function's return type
1959    /// ([`Self::current_fn_ret_type`]) for the return-position case
1960    /// (`return match …`). `None` ⇒ the caller emits the `interface{}` fallback.
1961    fn expected_iife_type(&self) -> Option<String> {
1962        match self.current_expected_type.as_deref() {
1963            Some(t) if t != "interface{}" => Some(t.to_string()),
1964            _ => self.current_fn_ret_type.clone(),
1965        }
1966    }
1967
1968    /// Populate [`Self::self_param_traits`] from the already-built
1969    /// [`Self::trait_decls`] registry. Call after `trait_decls` is set.
1970    fn derive_self_param_traits(&mut self) {
1971        for (name, info) in &self.trait_decls {
1972            if crate::generator::trait_uses_self_operand(info) {
1973                self.self_param_traits.insert(name.clone());
1974            }
1975        }
1976    }
1977
1978    /// Whether `name` is bound as a *local value* (parameter, `let`, match /
1979    /// guard bind, typed loop var) at the current emission point, in which case
1980    /// it must shadow a same-named public module function
1981    /// (Q-go-runtime-helper-shadowing). When any `core.string` item is imported
1982    /// the whole module is reached and its public fns (`lines`, `repeat`, …)
1983    /// enter [`Self::public_fns`]; without this check the identifier emitter
1984    /// PascalCased EVERY bare reference, so a `lines: List[String]` parameter
1985    /// used in `for line in lines` emitted `range Lines` — the helper
1986    /// *function* — which Go rejects. Bock scoping says the local wins; the
1987    /// checker already resolved it that way, so the Go spelling must too.
1988    ///
1989    /// Locals are tracked in two places, both keyed by the Go-escaped name and
1990    /// both populated only as emission *reaches* the binding (so a reference
1991    /// preceding a later same-named `let` still resolves to the module fn):
1992    /// [`Self::var_go_type`] (params on fn/method/lambda entry, typed loop vars,
1993    /// typed binds — saved/restored per scope) and the
1994    /// [`Self::go_declared_scopes`] frames (every `let`/bind the open blocks
1995    /// declared, seeded with the parameter names). Checked across *all* open
1996    /// frames — an outer fn-scope binding shadows inside nested blocks too.
1997    fn local_shadows_public_fn(&self, name: &str) -> bool {
1998        if !self.public_fns.contains(name) {
1999            return false;
2000        }
2001        let key = go_value_ident(name);
2002        self.var_go_type.contains_key(&key)
2003            || self
2004                .go_declared_scopes
2005                .iter()
2006                .any(|frame| frame.contains(&key))
2007    }
2008
2009    /// The Go identifier for a top-level Bock function reference, applying the
2010    /// public/private PascalCase/camelCase rule and then disambiguating a public
2011    /// name that collides with a top-level type. Go has a single namespace for
2012    /// types and functions, and PascalCasing collapses Bock's `key`/`Key` case
2013    /// distinction onto one identifier; when a public function's Go name equals a
2014    /// declared type name (`func Key` vs `type Key`), the function is suffixed
2015    /// with `Fn` (`KeyFn`). The same rule is applied at the declaration site and
2016    /// every call/reference site so they always agree.
2017    fn go_fn_name(&self, name: &str) -> String {
2018        if self.public_fns.contains(name) {
2019            let pascal = to_pascal_case(name);
2020            if self.type_names.contains(&pascal) {
2021                format!("{pascal}Fn")
2022            } else {
2023                pascal
2024            }
2025        } else {
2026            // Private fns and bare value references both route here; escape so a
2027            // `camelCase` name colliding with a Go keyword (`default`, `range`,
2028            // `type`, …) is mangled identically at the declaration and every
2029            // reference, keeping them in sync.
2030            go_value_ident(name)
2031        }
2032    }
2033
2034    /// Pre-scan every module's top-level type declarations (records, enums,
2035    /// traits, classes) into [`Self::type_names`], and every `public` top-level
2036    /// function name into [`Self::public_fns`], so [`Self::go_fn_name`] can
2037    /// detect a function-name/type-name collision at *any* call site — including
2038    /// a call that precedes the function's declaration in emission order.
2039    /// Mirrors the other pre-scans.
2040    fn collect_fn_and_type_names(&mut self, module: &AIRNode) {
2041        if let NodeKind::Module { items, .. } = &module.kind {
2042            for item in items {
2043                match &item.kind {
2044                    NodeKind::RecordDecl { name, .. }
2045                    | NodeKind::EnumDecl { name, .. }
2046                    | NodeKind::TraitDecl { name, .. }
2047                    | NodeKind::ClassDecl { name, .. } => {
2048                        self.type_names.insert(name.name.clone());
2049                    }
2050                    NodeKind::FnDecl {
2051                        visibility, name, ..
2052                    } if matches!(visibility, Visibility::Public) && name.name != "main" => {
2053                        self.public_fns.insert(name.name.clone());
2054                    }
2055                    _ => {}
2056                }
2057                // Record every *generic* top-level fn's signature (generic-param
2058                // names + each value param's declared type) so a call site can
2059                // specialise an untyped lambda argument to the concrete Go type
2060                // (see `fn_signatures`). Recorded regardless of visibility — the
2061                // embedded `core.iter` combinators are public, but a user's
2062                // private generic fn taking a lambda needs the same treatment.
2063                if let NodeKind::FnDecl {
2064                    name,
2065                    generic_params,
2066                    params,
2067                    ..
2068                } = &item.kind
2069                {
2070                    if !generic_params.is_empty() {
2071                        let gp_names: Vec<String> =
2072                            generic_params.iter().map(|p| p.name.name.clone()).collect();
2073                        let param_tys: Vec<Option<AIRNode>> = params
2074                            .iter()
2075                            .map(|p| match &p.kind {
2076                                NodeKind::Param { ty, .. } => ty.as_deref().cloned(),
2077                                _ => None,
2078                            })
2079                            .collect();
2080                        let ret_ty = if let NodeKind::FnDecl { return_type, .. } = &item.kind {
2081                            return_type.as_deref().cloned()
2082                        } else {
2083                            None
2084                        };
2085                        // A generic param whose sealed-core bound was lowered to a
2086                        // Go built-in constraint defeats Go's untyped-constant
2087                        // inference (GAP-C), so the call site must synthesise an
2088                        // explicit type arg — record the fn.
2089                        let has_sealed_bound = generic_params.iter().any(|p| {
2090                            p.bounds.iter().any(|b| {
2091                                let bn = b
2092                                    .segments
2093                                    .iter()
2094                                    .map(|s| s.name.as_str())
2095                                    .collect::<Vec<_>>()
2096                                    .join(".");
2097                                crate::generator::is_unimplemented_sealed_core_trait(
2098                                    &bn,
2099                                    &self.trait_decls,
2100                                )
2101                            })
2102                        });
2103                        if has_sealed_bound {
2104                            self.fn_sealed_bound.insert(name.name.clone());
2105                        }
2106                        self.fn_signatures
2107                            .insert(name.name.clone(), (gp_names, param_tys, ret_ty));
2108                    } else if let NodeKind::FnDecl { return_type, .. } = &item.kind {
2109                        // A *non-generic* fn: record its rendered Go return type so
2110                        // a call (`key(3)`) can be typed for homogeneous list-elem
2111                        // inference. Skip `Void`/`Unit` returns (no usable type).
2112                        if let Some(ret) = return_type.as_deref() {
2113                            if !Self::is_void_type(ret) {
2114                                self.fn_return_go_types
2115                                    .insert(name.name.clone(), self.type_to_go(ret));
2116                            }
2117                        }
2118                        // Record the value params' declared type nodes so an
2119                        // untyped lambda argument to a concrete `Fn(...)` param
2120                        // (`count_where(todos, (t) => t.done)`) can be specialised
2121                        // to `func(t Todo) bool` rather than `func(t interface{})`.
2122                        let param_tys: Vec<Option<AIRNode>> = params
2123                            .iter()
2124                            .map(|p| match &p.kind {
2125                                NodeKind::Param { ty, .. } => ty.as_deref().cloned(),
2126                                _ => None,
2127                            })
2128                            .collect();
2129                        if param_tys.iter().any(Option::is_some) {
2130                            self.fn_param_types.insert(name.name.clone(), param_tys);
2131                        }
2132                    }
2133                }
2134            }
2135        }
2136    }
2137
2138    /// Pre-scan a module's top-level `RecordDecl`s and, for each generic
2139    /// record, record which field's declared type is each generic param (in
2140    /// param order). A construction site then looks up the field value's Go
2141    /// type per param to emit the explicit `[arg, ...]` instantiation Go
2142    /// requires. Additive across the reached modules (mirrors the other `collect_*`).
2143    fn collect_record_param_fields(&mut self, module: &AIRModule) {
2144        let NodeKind::Module { items, .. } = &module.kind else {
2145            return;
2146        };
2147        for item in items {
2148            let NodeKind::RecordDecl {
2149                name,
2150                generic_params,
2151                fields,
2152                ..
2153            } = &item.kind
2154            else {
2155                continue;
2156            };
2157            // Record each `List[...]`-typed field's Go element type, keyed by
2158            // field name — used to type a `self.field.get(i)` list-method
2159            // receiver's closure inside the record's methods. Done for every
2160            // record (generic or not): a non-generic record may still hold a
2161            // `List[Int]` field whose method-side receiver needs `[]int64`.
2162            let list_fields: HashMap<String, String> = fields
2163                .iter()
2164                .filter_map(|f| {
2165                    Self::list_field_elem_type(&f.ty)
2166                        .map(|elem_ty| (f.name.name.clone(), self.ast_type_to_go(elem_ty)))
2167                })
2168                .collect();
2169            if !list_fields.is_empty() {
2170                self.record_field_list_elem
2171                    .insert(name.name.clone(), list_fields);
2172            }
2173            // Record each `Map[K, V]`-typed field's Go key/value types, keyed by
2174            // field name — used to type a `record.field.get(k)` map-method
2175            // receiver's inline closure (`map[K]V` / `K` / `V`) from the field's
2176            // declared types rather than the erased `map[interface{}]interface{}`.
2177            let map_fields: HashMap<String, (String, String)> = fields
2178                .iter()
2179                .filter_map(|f| {
2180                    Self::map_field_kv_type(&f.ty).map(|(k, v)| {
2181                        (
2182                            f.name.name.clone(),
2183                            (self.ast_type_to_go(k), self.ast_type_to_go(v)),
2184                        )
2185                    })
2186                })
2187                .collect();
2188            if !map_fields.is_empty() {
2189                self.record_field_map_kv
2190                    .insert(name.name.clone(), map_fields);
2191            }
2192            // Record EVERY field's Go type, keyed by field name — lets
2193            // `infer_go_expr_type` resolve a bare `obj.field` access (a
2194            // `.map((b) => b.id)` closure body) to the field's concrete Go type,
2195            // sizing the result slice concretely rather than as `[]interface{}`.
2196            let field_go_types: HashMap<String, String> = fields
2197                .iter()
2198                .map(|f| (f.name.name.clone(), self.ast_type_to_go(&f.ty)))
2199                .collect();
2200            if !field_go_types.is_empty() {
2201                self.record_field_go_type
2202                    .insert(name.name.clone(), field_go_types);
2203            }
2204            if generic_params.is_empty() {
2205                continue;
2206            }
2207            self.record_generic_param_names.insert(
2208                name.name.clone(),
2209                generic_params.iter().map(|p| p.name.name.clone()).collect(),
2210            );
2211            let per_param: Vec<Option<String>> = generic_params
2212                .iter()
2213                .map(|gp| {
2214                    fields
2215                        .iter()
2216                        .find(|f| Self::ast_type_is_param(&f.ty, &gp.name.name))
2217                        .map(|f| f.name.name.clone())
2218                })
2219                .collect();
2220            self.record_param_fields
2221                .insert(name.name.clone(), per_param);
2222        }
2223    }
2224
2225    /// If `ty` is a `List[Elem]` named type, return its element `TypeExpr`,
2226    /// else `None`. Used to record a record field's list element type for
2227    /// method-side receiver typing.
2228    fn list_field_elem_type(ty: &TypeExpr) -> Option<&TypeExpr> {
2229        match ty {
2230            TypeExpr::Named { path, args, .. }
2231                if args.len() == 1 && path.segments.last().is_some_and(|s| s.name == "List") =>
2232            {
2233                args.first()
2234            }
2235            _ => None,
2236        }
2237    }
2238
2239    /// The `(key, value)` type expressions of a `Map[K, V]`-typed field, or
2240    /// `None` for any other type. The `Map` analogue of
2241    /// [`Self::list_field_elem_type`]; used to populate
2242    /// [`Self::record_field_map_kv`].
2243    fn map_field_kv_type(ty: &TypeExpr) -> Option<(&TypeExpr, &TypeExpr)> {
2244        match ty {
2245            TypeExpr::Named { path, args, .. }
2246                if args.len() == 2 && path.segments.last().is_some_and(|s| s.name == "Map") =>
2247            {
2248                Some((args.first()?, args.get(1)?))
2249            }
2250            _ => None,
2251        }
2252    }
2253
2254    /// True when `ty` is a bare named type whose single segment is `param`
2255    /// (i.e. the field is declared with exactly the generic param `T`, not
2256    /// `List[T]` or some other composite).
2257    fn ast_type_is_param(ty: &TypeExpr, param: &str) -> bool {
2258        matches!(
2259            ty,
2260            TypeExpr::Named { path, args, .. }
2261                if args.is_empty()
2262                    && path.segments.len() == 1
2263                    && path.segments[0].name == param
2264        )
2265    }
2266
2267    /// Variant info for `path` when its last segment is a registered *user*
2268    /// enum variant (built-in Optional/Result pre-seeds excluded — Optional has
2269    /// its own `__bockOption` runtime, handled by the bespoke `go_match_is_*`
2270    /// paths).
2271    fn user_variant_for_path(
2272        &self,
2273        path: &bock_ast::TypePath,
2274    ) -> Option<&crate::generator::EnumVariantInfo> {
2275        let info = crate::generator::registered_variant(&self.enum_variants, path)?;
2276        if matches!(info.enum_name.as_str(), "Optional" | "Result") {
2277            return None;
2278        }
2279        Some(info)
2280    }
2281
2282    /// As [`Self::user_variant_for_path`] but keyed by a bare identifier name.
2283    fn user_variant_for_name(&self, name: &str) -> Option<&crate::generator::EnumVariantInfo> {
2284        let info = self.enum_variants.get(name)?;
2285        if matches!(info.enum_name.as_str(), "Optional" | "Result") {
2286            return None;
2287        }
2288        Some(info)
2289    }
2290
2291    /// True when the real `core.compare.Ordering` enum is reachable in this
2292    /// program (its `Less` variant is a registered user enum variant). When
2293    /// `core.compare` is `use`d, the actual `enum Ordering` decl is emitted; its
2294    /// `Less`/`Equal`/`Greater` references and matches then use the user-enum
2295    /// representation (sealed-interface variant structs `OrderingLess{}`), not
2296    /// the prelude `__bockOrdering` value runtime used when the enum is *not*
2297    /// reachable (e.g. a bare primitive `compare`).
2298    fn ordering_enum_reachable(&self) -> bool {
2299        self.enum_variants
2300            .get("Less")
2301            .is_some_and(|info| info.enum_name == "Ordering")
2302    }
2303
2304    /// True if every arm of a `match` is a registered user enum variant pattern
2305    /// (constructor / record / unit), so the match lowers to a Go *type-switch*
2306    /// over the sealed-interface concrete types with field extraction.
2307    fn go_match_is_user_enum(&self, arms: &[AIRNode]) -> bool {
2308        let mut saw_variant = false;
2309        for arm in arms {
2310            let NodeKind::MatchArm { pattern, .. } = &arm.kind else {
2311                continue;
2312            };
2313            match &pattern.kind {
2314                NodeKind::ConstructorPat { path, .. } | NodeKind::RecordPat { path, .. }
2315                    if self.user_variant_for_path(path).is_some() =>
2316                {
2317                    saw_variant = true;
2318                }
2319                // Any constructor / record pattern that is NOT a registered
2320                // user variant disqualifies the type-switch lowering.
2321                NodeKind::ConstructorPat { .. } | NodeKind::RecordPat { .. } => return false,
2322                // A trailing `_` / bind arm is a permissible default.
2323                NodeKind::WildcardPat | NodeKind::BindPat { .. } => {}
2324                _ => return false,
2325            }
2326        }
2327        saw_variant
2328    }
2329
2330    /// The owning enum name of a user-enum match's arms, if any arm names a
2331    /// registered variant. Used to recover the enum's generic instantiation
2332    /// (`Box[int64]`) so a type-switch `case`/type-assert spells the concrete
2333    /// variant struct (`case BoxFull[int64]:`). All arms of one match share an
2334    /// enum (a type-switch on a single sealed-interface value), so the first
2335    /// variant-bearing arm decides.
2336    fn match_owner_enum_name(&self, arms: &[AIRNode]) -> Option<String> {
2337        for arm in arms {
2338            let NodeKind::MatchArm { pattern, .. } = &arm.kind else {
2339                continue;
2340            };
2341            let path = match &pattern.kind {
2342                NodeKind::ConstructorPat { path, .. } | NodeKind::RecordPat { path, .. } => path,
2343                _ => continue,
2344            };
2345            if let Some(info) = self.user_variant_for_path(path) {
2346                return Some(info.enum_name.clone());
2347            }
2348        }
2349        None
2350    }
2351
2352    /// The concrete generic type-arg suffix (`[int64]`) to apply to the variant
2353    /// structs of a user-enum `match`, recovered from the scrutinee's inferred Go
2354    /// type (`Box[int64]`) and the match's owning enum. `""` when the scrutinee
2355    /// type is unknown, the enum is non-generic, or the inferred type is not
2356    /// exactly `<enum>[...]`. Drives [`Self::current_match_enum_type_args`].
2357    fn match_enum_type_arg_suffix(&self, scrutinee: &AIRNode, arms: &[AIRNode]) -> String {
2358        let Some(enum_name) = self.match_owner_enum_name(arms) else {
2359            return String::new();
2360        };
2361        match self.infer_go_expr_type(scrutinee) {
2362            Some(full) => self.enum_variant_type_arg_suffix(&enum_name, &full),
2363            None => String::new(),
2364        }
2365    }
2366
2367    /// True if any arm of a user-enum type-switch binds a payload field from the
2368    /// concrete `__v` (a `ConstructorPat`/`RecordPat` with at least one non-`_`
2369    /// sub-pattern). When no arm does, the statement-position type-switch binds
2370    /// `__v` but never reads it — Go's "declared and not used" — unless a
2371    /// `default: panic(... __v)` consumes it. See [`Self::emit_match`].
2372    fn go_user_enum_match_binds_payload(arms: &[AIRNode]) -> bool {
2373        arms.iter().any(|arm| {
2374            let NodeKind::MatchArm { pattern, .. } = &arm.kind else {
2375                return false;
2376            };
2377            match &pattern.kind {
2378                NodeKind::ConstructorPat { fields, .. } => fields
2379                    .iter()
2380                    .any(|f| !matches!(f.kind, NodeKind::WildcardPat)),
2381                NodeKind::RecordPat { fields, .. } => fields.iter().any(|f| {
2382                    f.pattern
2383                        .as_ref()
2384                        .is_none_or(|p| !matches!(p.kind, NodeKind::WildcardPat))
2385                }),
2386                _ => false,
2387            }
2388        })
2389    }
2390
2391    /// True if any arm's top-level pattern is a bare `BindPat` (`x => …`,
2392    /// `mut x => …`). Such a match cannot use the value-switch IIFE in expression
2393    /// position: a bind has no value to `case` on, so the switch lowering emits
2394    /// the broken `case interface{}:` and drops the bound name. The
2395    /// expression-position emitter routes these to the if-chain IIFE instead
2396    /// (which binds `x := root` in an unconditional `else`). Statement position is
2397    /// unaffected — `emit_match`'s `value_switch_binds` already handles it via the
2398    /// `switch __v := scrutinee; __v` form. Mirrors the `match_needs_ifchain`
2399    /// gate without touching that shared single-discriminant fast-path.
2400    fn go_value_match_has_bind_arm(arms: &[AIRNode]) -> bool {
2401        arms.iter().any(|arm| {
2402            matches!(
2403                &arm.kind,
2404                NodeKind::MatchArm { pattern, .. }
2405                    if matches!(pattern.kind, NodeKind::BindPat { .. })
2406            )
2407        })
2408    }
2409
2410    /// True if any arm matches a *plain* (non-enum-variant) record with only
2411    /// bind/wildcard fields (`Point { x, .. }`, `Point { x, y }`). Such an arm is
2412    /// not "structured" by `match_needs_ifchain` (no nested sub-pattern), so a
2413    /// value-position match made solely of these stays on the value-switch
2414    /// fast-path — which emits the broken `case Point:` (a Go *type* in expression
2415    /// position) and drops the field binds (`undefined: x`). A plain record is a
2416    /// concrete struct, not a sealed-interface value, so it has no value/type to
2417    /// switch on at all; route the match to the if-chain IIFE, whose
2418    /// `pattern_test_go` / `collect_binds_go` read each field directly off
2419    /// `access.<Field>`. (A record arm with a *literal* field — `Point { x: 0 }` —
2420    /// is already structured, so `match_needs_ifchain` diverts it; this only
2421    /// covers the all-bind/wildcard plain-record arm.) Does not touch the shared
2422    /// `match_needs_ifchain`.
2423    fn go_value_match_has_plain_record_arm(&self, arms: &[AIRNode]) -> bool {
2424        arms.iter().any(|arm| {
2425            let NodeKind::MatchArm { pattern, .. } = &arm.kind else {
2426                return false;
2427            };
2428            matches!(&pattern.kind, NodeKind::RecordPat { path, .. }
2429                if self.user_variant_for_path(path).is_none())
2430        })
2431    }
2432
2433    /// Pre-scan the module for top-level `async fn` names. Must be populated
2434    /// before any Call node is emitted so the Async-suffix rewrite at call
2435    /// sites covers both forward and backward references within the module.
2436    fn collect_async_fns(&mut self, module: &AIRNode) {
2437        if let NodeKind::Module { items, .. } = &module.kind {
2438            for item in items {
2439                if let NodeKind::FnDecl {
2440                    is_async: true,
2441                    name,
2442                    ..
2443                } = &item.kind
2444                {
2445                    self.async_fns.insert(name.name.clone());
2446                }
2447            }
2448        }
2449    }
2450
2451    /// Pre-scan all impl/class/trait blocks for `public` method names so call
2452    /// sites can match the Go method casing (PascalCase public, camelCase
2453    /// private).
2454    ///
2455    /// Trait methods — both those declared in a `TraitDecl` and those of an
2456    /// `impl Trait for Type` block — are recorded *regardless of Bock
2457    /// visibility*: Go interface methods are always emitted exported
2458    /// (PascalCase, see the `TraitDecl` emission), so the method must be
2459    /// PascalCased everywhere (interface signature, receiver method, and call
2460    /// site) for the type to satisfy the interface. A `private` trait default
2461    /// method would otherwise be PascalCased in the interface but camelCased at
2462    /// the call site, and the call would not resolve. Inherent (`impl Type`)
2463    /// and class methods keep the public-only rule.
2464    fn collect_methods(&mut self, module: &AIRNode) {
2465        // Collect every record/class field's PascalCased Go name so
2466        // `go_method_name` can detect a field/method name collision Go
2467        // forbids on a struct (`SimpleError { message }` + a `message`
2468        // method). The shared collector (used identically by js/ts/py) walks
2469        // every record/class regardless of where the colliding method is
2470        // declared (a separate `impl` block). `collect_methods` is called once
2471        // per reachable module to build a *program-wide* set on the template
2472        // ctx (see `generate_project`), so we extend rather than replace.
2473        self.record_field_names
2474            .extend(crate::generator::collect_record_field_names(
2475                module,
2476                to_pascal_case,
2477            ));
2478        if let NodeKind::Module { items, .. } = &module.kind {
2479            for item in items {
2480                // `inherent_target` is `Some(type name)` for an inherent (`impl
2481                // Type`, no trait) or class block — used to record
2482                // `(type, method)` so a redundant same-named trait-impl forwarder
2483                // can be skipped. A trait impl (`impl Trait for Type`) is not an
2484                // inherent definition (it is the forwarder we may skip).
2485                let (methods, always_export, inherent_target) = match &item.kind {
2486                    NodeKind::ImplBlock {
2487                        methods,
2488                        trait_path,
2489                        target,
2490                        ..
2491                    } => {
2492                        let inherent = if trait_path.is_none() {
2493                            Some(self.type_expr_to_string(target))
2494                        } else {
2495                            None
2496                        };
2497                        (methods, trait_path.is_some(), inherent)
2498                    }
2499                    NodeKind::TraitDecl { methods, .. } => (methods, true, None),
2500                    NodeKind::ClassDecl { methods, name, .. } => {
2501                        (methods, false, Some(name.name.clone()))
2502                    }
2503                    _ => continue,
2504                };
2505                for m in methods {
2506                    if let NodeKind::FnDecl {
2507                        visibility,
2508                        name,
2509                        generic_params,
2510                        ..
2511                    } = &m.kind
2512                    {
2513                        if always_export || matches!(visibility, Visibility::Public) {
2514                            self.public_methods.insert(name.name.clone());
2515                        }
2516                        if let Some(ty) = &inherent_target {
2517                            // Key on the PascalCased Go method name: a trait
2518                            // declares its methods exported (PascalCase), so a
2519                            // skip check from the trait-impl side compares against
2520                            // that casing. The inherent method itself is exported
2521                            // to the same Go name when its name is in
2522                            // `public_methods` (see `emit_method_body`).
2523                            self.inherent_methods
2524                                .insert((ty.clone(), to_pascal_case(&name.name)));
2525                            // DQ28: an inherent/class method that declares its own
2526                            // type parameters (`Box[T].map[U]`) cannot be a Go
2527                            // method (Go forbids method type params). Record it for
2528                            // free-function lowering, keyed by the Bock method
2529                            // name → owning type. Poison the entry if a second type
2530                            // declares a generic method of the same name (ambiguous
2531                            // at the by-name call site): set the value to a sentinel
2532                            // so the lookup treats it as absent.
2533                            if !generic_params.is_empty() {
2534                                use std::collections::hash_map::Entry;
2535                                match self.method_freefn_lowered.entry(name.name.clone()) {
2536                                    Entry::Vacant(e) => {
2537                                        e.insert(ty.clone());
2538                                    }
2539                                    Entry::Occupied(mut e) => {
2540                                        if e.get() != ty {
2541                                            // Ambiguous: two types, same generic
2542                                            // method name. Poison with a sentinel
2543                                            // that names no real type.
2544                                            e.insert(String::new());
2545                                        }
2546                                    }
2547                                }
2548                            }
2549                        }
2550                    }
2551                }
2552            }
2553        }
2554    }
2555
2556    /// The Go method name for a Bock method, applying the public/private
2557    /// PascalCase/camelCase rule and then disambiguating a public method whose
2558    /// PascalCased name collides with a struct field name. Go forbids a struct
2559    /// having a field and a method with the same name, so when a public method's
2560    /// Go name (`Message`) equals a record/class field name (`SimpleError`'s
2561    /// `message` field), the method is suffixed `Method` (`MessageMethod`). The
2562    /// same rule is applied at the trait-interface declaration, the receiver
2563    /// method, and every call site so they always agree. Private methods are
2564    /// camelCased and never collide with a (PascalCased) field name.
2565    fn go_method_name(&self, name: &str, is_public: bool) -> String {
2566        if is_public {
2567            // Shared collision policy (js/ts/py route through the same helper):
2568            // a public method whose PascalCased name equals a field name gets a
2569            // `Method` suffix (`Message` → `MessageMethod`).
2570            crate::generator::disambiguate_method_name(
2571                to_pascal_case(name),
2572                &self.record_field_names,
2573                "Method",
2574            )
2575        } else {
2576            to_camel_case(name)
2577        }
2578    }
2579
2580    /// The owning Go type name of a method that is *free-function-lowered* for
2581    /// DQ28 (a method with its own type parameters, e.g. `Box[T].map[U]`), or
2582    /// `None` when the method name is not lowered or is ambiguous (the poison
2583    /// sentinel — an empty type name — reads as absent). When `Some(ty)`, a call
2584    /// site `recv.method(args)` lowers to `<FreeFnName>(recv, args)` and the
2585    /// declaration emits a free function instead of a Go method.
2586    fn freefn_lowered_type(&self, method_name: &str) -> Option<&str> {
2587        self.method_freefn_lowered
2588            .get(method_name)
2589            .map(String::as_str)
2590            .filter(|ty| !ty.is_empty())
2591    }
2592
2593    /// The Go free-function name a DQ28-lowered method lowers to:
2594    /// `<TypeName>_<MethodGoName>` (`Box` + `Map` → `Box_Map`). The method name
2595    /// is PascalCased via [`Self::go_method_name`] (a public method matches its
2596    /// every call site; a private one camelCases) so the declaration and call
2597    /// site always agree. The `<Type>_` prefix guarantees collision-free naming
2598    /// across types that share a method name.
2599    fn freefn_lowered_name(&self, type_name: &str, method_name: &str, is_public: bool) -> String {
2600        format!(
2601            "{type_name}_{}",
2602            self.go_method_name(method_name, is_public)
2603        )
2604    }
2605
2606    /// Pre-scan top-level functions whose declared return type is `Optional[T]`,
2607    /// recording `fn name → Go element type` of `T`. This lets a `match` whose
2608    /// scrutinee is a call to such a function (`match next(it) { Some(x) => ...
2609    /// }`) type-assert the bound payload to its concrete type. Must run before
2610    /// any match is emitted, so it covers forward references within the module.
2611    /// Pre-scan every top-level `type X = …` alias, recording `X → underlying
2612    /// type AIR node`. Lets the emitter resolve an alias *name* to its target Go
2613    /// type ([`Self::resolve_type_alias`]) wherever it appears — a function
2614    /// return/param type, or a `Result`/`Optional` element scan — so an alias to a
2615    /// runtime container (`type ParseResult = Result[...]`) lowers identically to
2616    /// the inlined container. Only non-generic aliases are recorded (a generic
2617    /// alias `type Pair[A, B] = (A, B)` would need substitution the emitter does
2618    /// not perform; it is left to fall through to its existing rendering).
2619    fn collect_type_aliases(&mut self, module: &AIRNode) {
2620        if let NodeKind::Module { items, .. } = &module.kind {
2621            for item in items {
2622                if let NodeKind::TypeAlias {
2623                    name,
2624                    generic_params,
2625                    ty,
2626                    ..
2627                } = &item.kind
2628                {
2629                    if generic_params.is_empty() {
2630                        self.type_aliases
2631                            .entry(name.name.clone())
2632                            .or_insert_with(|| (**ty).clone());
2633                    }
2634                }
2635            }
2636        }
2637    }
2638
2639    /// If `node` is a `TypeNamed` naming a recorded non-generic type alias, return
2640    /// its underlying type AIR node (following at most one level — Bock aliases do
2641    /// not chain in practice, and a bounded depth avoids any cycle). `None` for a
2642    /// non-alias or non-`TypeNamed` node.
2643    fn resolve_type_alias(&self, node: &AIRNode) -> Option<&AIRNode> {
2644        if let NodeKind::TypeNamed { path, args } = &node.kind {
2645            if args.is_empty() {
2646                if let Some(seg) = path.segments.last() {
2647                    return self.type_aliases.get(&seg.name);
2648                }
2649            }
2650        }
2651        None
2652    }
2653
2654    fn collect_optional_returns(&mut self, module: &AIRNode) {
2655        if let NodeKind::Module { items, .. } = &module.kind {
2656            for item in items {
2657                // Effect operations are dispatched by bare name (`read(key)` with
2658                // the handler resolved implicitly), so the AIR lowers a call to one
2659                // as `Call(Identifier "read", ...)` — indistinguishable from a free
2660                // fn at the call site. Record each effect op's `Optional`/`Result`
2661                // return into the by-name maps so a `match` on such a call (or on a
2662                // binding of it) type-asserts the boxed payload concretely. Without
2663                // this, the bound `interface{}` payload fails a later concrete use
2664                // (effect-showcase: `raw := storage.read(key); match raw { Some(v)
2665                // => return v }`, `v` wanted as `string`).
2666                if let NodeKind::EffectDecl { operations, .. } = &item.kind {
2667                    for op in operations {
2668                        if let NodeKind::FnDecl {
2669                            name,
2670                            return_type: Some(rt),
2671                            ..
2672                        } = &op.kind
2673                        {
2674                            if let Some(elem) = self.optional_elem_go_type(rt) {
2675                                self.fn_optional_ret_elem
2676                                    .entry(name.name.clone())
2677                                    .or_insert(elem);
2678                            }
2679                            if let Some(elems) = self.result_elem_go_types(rt) {
2680                                self.fn_result_ret_elem
2681                                    .entry(name.name.clone())
2682                                    .or_insert(elems);
2683                            }
2684                        }
2685                    }
2686                }
2687                if let NodeKind::FnDecl {
2688                    name,
2689                    return_type: Some(rt),
2690                    ..
2691                } = &item.kind
2692                {
2693                    if let Some(elem) = self.optional_elem_go_type(rt) {
2694                        self.fn_optional_ret_elem.insert(name.name.clone(), elem);
2695                    }
2696                    // Same pre-scan for `Result[T, E]` returns, so a `match
2697                    // parse(s) { Ok(n) => ... }` on a call scrutinee asserts the
2698                    // bound payload's Ok/Err type (mirrors the Optional path).
2699                    if let Some(elems) = self.result_elem_go_types(rt) {
2700                        self.fn_result_ret_elem.insert(name.name.clone(), elems);
2701                    }
2702                }
2703            }
2704        }
2705    }
2706
2707    /// Pre-scan every impl/class/trait block for methods whose declared return
2708    /// type is `Optional[T]`, recording `method name → Go element type` of `T`.
2709    /// This lets a `match` whose scrutinee is a method call
2710    /// (`match it.next() { Some(x) => ... }`) type-assert the bound payload to
2711    /// its concrete element type — the shape `for x in <user-Iterable>` desugars
2712    /// to (a `loop`/`while` over `it.next(): T?`). Without it the payload stays
2713    /// the runtime `interface{}` and any typed use (`sum + x`) fails `go build`.
2714    ///
2715    /// Keyed by method name only — the Go backend works from the AIR, not the
2716    /// checker's per-type `method_types`. If the same method name appears on two
2717    /// types with *different* Optional element types, the entry is poisoned (its
2718    /// value cleared and a sentinel recorded) so resolution returns `None` and
2719    /// the payload safely falls back to `interface{}`. Must run before any match
2720    /// is emitted so it covers forward references within the module.
2721    fn collect_method_optional_returns(&mut self, module: &AIRNode) {
2722        // Methods sharing a name but disagreeing on element type are ambiguous;
2723        // track them here so the final map omits them entirely.
2724        let mut poisoned: HashSet<String> = HashSet::new();
2725        let mut poisoned_record: HashSet<String> = HashSet::new();
2726        let mut poisoned_go: HashSet<String> = HashSet::new();
2727        if let NodeKind::Module { items, .. } = &module.kind {
2728            for item in items {
2729                // The item's in-scope generic-param names: an impl's own plus
2730                // the target record's (`impl ListIterator { ... }` inherits the
2731                // `[T]` from `record ListIterator[T]`); a trait's declared
2732                // params. A method whose record return names one of these is
2733                // *not* a concrete return (it is the generic declaration), so it
2734                // is excluded from `method_ret_record_args`.
2735                let (methods, item_params): (&Vec<AIRNode>, Vec<String>) = match &item.kind {
2736                    NodeKind::ImplBlock {
2737                        methods,
2738                        generic_params,
2739                        target,
2740                        ..
2741                    } => {
2742                        let mut ps: Vec<String> =
2743                            generic_params.iter().map(|p| p.name.name.clone()).collect();
2744                        let target_name = self.type_expr_to_string(target);
2745                        if let Some(decl) = self.generic_decls.get(&target_name) {
2746                            ps.extend(decl.iter().map(|p| p.name.name.clone()));
2747                        }
2748                        (methods, ps)
2749                    }
2750                    NodeKind::ClassDecl {
2751                        methods,
2752                        generic_params,
2753                        ..
2754                    }
2755                    | NodeKind::TraitDecl {
2756                        methods,
2757                        generic_params,
2758                        ..
2759                    } => (
2760                        methods,
2761                        generic_params.iter().map(|p| p.name.name.clone()).collect(),
2762                    ),
2763                    _ => continue,
2764                };
2765                for m in methods {
2766                    if let NodeKind::FnDecl {
2767                        name,
2768                        return_type: Some(rt),
2769                        ..
2770                    } = &m.kind
2771                    {
2772                        if let Some(elem) = self.optional_elem_go_type(rt) {
2773                            match self.method_optional_ret_elem.get(&name.name) {
2774                                Some(existing) if *existing != elem => {
2775                                    poisoned.insert(name.name.clone());
2776                                }
2777                                _ => {
2778                                    self.method_optional_ret_elem
2779                                        .insert(name.name.clone(), elem);
2780                                }
2781                            }
2782                        }
2783                        // A method returning a *concrete* generic-record apply
2784                        // (`iter() -> ListIterator[Int]`, no remaining param) is
2785                        // recorded so an untyped binding of its call resolves the
2786                        // record args (`for x in bag` → `__it := bag.Iter()`).
2787                        // A return still naming an in-scope generic param (the
2788                        // `Iterable[T]` trait decl's `iter() -> ListIterator[T]`)
2789                        // is skipped — it is the generic signature, not a
2790                        // concrete return, and would falsely poison the concrete
2791                        // impl's entry.
2792                        if let Some(args) = self.record_type_args(rt) {
2793                            let is_concrete =
2794                                !args.1.iter().any(|a| item_params.iter().any(|p| p == a));
2795                            if is_concrete {
2796                                match self.method_ret_record_args.get(&name.name) {
2797                                    Some(existing) if *existing != args => {
2798                                        poisoned_record.insert(name.name.clone());
2799                                    }
2800                                    _ => {
2801                                        self.method_ret_record_args.insert(name.name.clone(), args);
2802                                    }
2803                                }
2804                            }
2805                        }
2806                        // Record the method's concrete Go return type, so
2807                        // `infer_go_expr_type` can type a `recv.method()` call
2808                        // (chiefly a `.map`/`.filter` closure body). A return
2809                        // type that still names an in-scope generic param is
2810                        // skipped — it is the generic signature, not concrete, and
2811                        // the calling site (a different fn) has no such `T`.
2812                        if !Self::type_mentions_params(rt, &item_params) {
2813                            let go_ty = self.type_to_go(rt);
2814                            match self.method_return_go_types.get(&name.name) {
2815                                Some(existing) if *existing != go_ty => {
2816                                    poisoned_go.insert(name.name.clone());
2817                                }
2818                                _ => {
2819                                    self.method_return_go_types.insert(name.name.clone(), go_ty);
2820                                }
2821                            }
2822                        }
2823                    }
2824                }
2825            }
2826        }
2827        for name in &poisoned {
2828            self.method_optional_ret_elem.remove(name);
2829        }
2830        for name in &poisoned_record {
2831            self.method_ret_record_args.remove(name);
2832        }
2833        for name in &poisoned_go {
2834            self.method_return_go_types.remove(name);
2835        }
2836    }
2837
2838    /// If `node` is an `Optional[T]` type expression, return the Go type of its
2839    /// element `T`; otherwise `None`. Used to type-assert the `interface{}`
2840    /// payload of the Go Optional runtime back to its concrete element type at
2841    /// `match` arms. The element type is reachable structurally here because it
2842    /// lives in the `TypeOptional`/`Optional`-named node, unlike at the
2843    /// scrutinee expression (whose carried `type_info` is a stub).
2844    fn optional_elem_go_type(&self, node: &AIRNode) -> Option<String> {
2845        // See through a `type X = Optional[T]` alias (mirrors
2846        // `result_elem_go_types`).
2847        if let Some(target) = self.resolve_type_alias(node) {
2848            return self.optional_elem_go_type(target);
2849        }
2850        match &node.kind {
2851            NodeKind::TypeOptional { inner } => Some(self.type_to_go(inner)),
2852            NodeKind::TypeNamed { path, args } => {
2853                let is_optional = path.segments.last().is_some_and(|s| s.name == "Optional");
2854                if is_optional {
2855                    Some(
2856                        args.first()
2857                            .map_or_else(|| "interface{}".to_string(), |a| self.type_to_go(a)),
2858                    )
2859                } else {
2860                    None
2861                }
2862            }
2863            _ => None,
2864        }
2865    }
2866
2867    /// If `node` is a `Result[T, E]` type expression, return the Go types of its
2868    /// `Ok` and `Err` payloads `(T, E)`; otherwise `None`. The Result analogue of
2869    /// [`Self::optional_elem_go_type`]: used to type-assert the `interface{}`
2870    /// payload of the Go Result runtime back to its concrete Ok/Err type at a
2871    /// `match` arm. A missing arg defaults to `interface{}`.
2872    fn result_elem_go_types(&self, node: &AIRNode) -> Option<(String, String)> {
2873        // See through a `type X = Result[T, E]` alias so a fn declared to return
2874        // the alias still records its Ok/Err payload types.
2875        if let Some(target) = self.resolve_type_alias(node) {
2876            return self.result_elem_go_types(target);
2877        }
2878        if let NodeKind::TypeNamed { path, args } = &node.kind {
2879            if path.segments.last().is_some_and(|s| s.name == "Result") {
2880                let ok = args
2881                    .first()
2882                    .map_or_else(|| "interface{}".to_string(), |a| self.type_to_go(a));
2883                let err = args
2884                    .get(1)
2885                    .map_or_else(|| "interface{}".to_string(), |a| self.type_to_go(a));
2886                return Some((ok, err));
2887            }
2888        }
2889        None
2890    }
2891
2892    /// If `node` is a `List[T]` type expression, return the Go type of its
2893    /// element `T`; otherwise `None`. The read-only `List` built-ins
2894    /// `get`/`first`/`last` return `Optional[T]` over the list element, so a
2895    /// `match` on such a call must type-assert the `interface{}` payload to this
2896    /// element type. Reached structurally from the receiver's declared
2897    /// `List[T]` type (its element is unrecoverable from the runtime
2898    /// `[]interface{}` value alone).
2899    fn list_elem_go_type(&self, node: &AIRNode) -> Option<String> {
2900        if let NodeKind::TypeNamed { path, args } = &node.kind {
2901            if path.segments.last().is_some_and(|s| s.name == "List") {
2902                return args.first().map(|a| self.type_to_go(a));
2903            }
2904        }
2905        None
2906    }
2907
2908    /// If `node` is a `Map[K, V]` type expression, return the Go types of its
2909    /// key and value `(K, V)`; otherwise `None`. The `Map` analogue of
2910    /// [`Self::list_elem_go_type`]: the built-in `Map` methods lower to inline
2911    /// closures over the concretely-typed receiver `map[K]V`, so a typed `let m:
2912    /// Map[K, V]` binding records these into [`Self::var_map_kv`]. A missing arg
2913    /// defaults to `interface{}`.
2914    fn map_kv_go_types(&self, node: &AIRNode) -> Option<(String, String)> {
2915        if let NodeKind::TypeNamed { path, args } = &node.kind {
2916            if path.segments.last().is_some_and(|s| s.name == "Map") {
2917                let k = args
2918                    .first()
2919                    .map_or_else(|| "interface{}".to_string(), |a| self.type_to_go(a));
2920                let v = args
2921                    .get(1)
2922                    .map_or_else(|| "interface{}".to_string(), |a| self.type_to_go(a));
2923                return Some((k, v));
2924            }
2925        }
2926        None
2927    }
2928
2929    /// If `node` is a `Set[E]` type expression, return the Go type of its
2930    /// element `E`; otherwise `None`. The `Set` analogue of
2931    /// [`Self::list_elem_go_type`], recorded into [`Self::var_set_elem`].
2932    fn set_elem_go_type(&self, node: &AIRNode) -> Option<String> {
2933        if let NodeKind::TypeNamed { path, args } = &node.kind {
2934            if path.segments.last().is_some_and(|s| s.name == "Set") {
2935                return Some(
2936                    args.first()
2937                        .map_or_else(|| "interface{}".to_string(), |a| self.type_to_go(a)),
2938                );
2939            }
2940        }
2941        None
2942    }
2943
2944    /// If `node` is an `Optional[T]` (or `T?`) type expression, return its inner
2945    /// `T` type node. The *node*-returning analogue of
2946    /// [`Self::optional_elem_go_type`]: lets the pattern recursion peel one
2947    /// Optional layer and keep descending a nested constructor pattern
2948    /// (`Some(Ok((a, b)))`) so a leaf tuple pattern lands on a concrete tuple
2949    /// type node. Sees through a `type X = Optional[T]` alias.
2950    fn optional_inner_type_node<'a>(&'a self, node: &'a AIRNode) -> Option<&'a AIRNode> {
2951        if let Some(target) = self.resolve_type_alias(node) {
2952            return self.optional_inner_type_node(target);
2953        }
2954        match &node.kind {
2955            NodeKind::TypeOptional { inner } => Some(inner),
2956            NodeKind::TypeNamed { path, args }
2957                if path.segments.last().is_some_and(|s| s.name == "Optional") =>
2958            {
2959                args.first()
2960            }
2961            _ => None,
2962        }
2963    }
2964
2965    /// Clone `ret`'s function-type node when the declared return type is a
2966    /// function type (`Fn(Int) -> Int`), else `None`. Kept on
2967    /// [`Self::current_fn_ret_type_node`] so a lambda in tail-return position can
2968    /// take its param/return Go types from the declared function type. (Does not
2969    /// peel a `type` alias to a function type — a function returning such an
2970    /// alias is vanishingly rare in the v1 examples and the lambda still emits
2971    /// correctly via inference, just un-typed.)
2972    fn fn_type_ret_node(ret: Option<&AIRNode>) -> Option<AIRNode> {
2973        let node = ret?;
2974        if matches!(node.kind, NodeKind::TypeFunction { .. }) {
2975            Some(node.clone())
2976        } else {
2977            None
2978        }
2979    }
2980
2981    /// The `(param_go_types, return_go_type)` of a `TypeFunction` node, each
2982    /// rendered as Go. Used to type a lambda emitted in return position from the
2983    /// enclosing function's declared `Fn(...) -> ...` return type. Returns `None`
2984    /// when `node` is not a function type.
2985    fn fn_type_go_signature(&self, node: &AIRNode) -> Option<(Vec<String>, String)> {
2986        if let NodeKind::TypeFunction { params, ret, .. } = &node.kind {
2987            let param_tys = params.iter().map(|p| self.type_to_go(p)).collect();
2988            return Some((param_tys, self.type_to_go(ret)));
2989        }
2990        None
2991    }
2992
2993    /// When `tail` is a bare lambda being emitted in *tail-return position* and
2994    /// the enclosing function's declared return type is a function type
2995    /// (`fn compose(...) -> Fn(Int) -> Int { (x) => f(g(x)) }`), pin the lambda's
2996    /// param/return Go types from that declared type, so the emitted closure is
2997    /// `func(x int64) int64` rather than the inference fallback
2998    /// `func(x interface{}) interface{}` (not assignable to the declared return).
2999    /// Returns the saved `(expected_lambda_param_types, forced_lambda_ret)` so the
3000    /// caller restores them after the emit; a no-op (saved state unchanged) when
3001    /// the tail is not a lambda or the return type is not a function type.
3002    #[allow(clippy::type_complexity)]
3003    fn pin_return_lambda_types(&mut self, tail: &AIRNode) -> (Option<Vec<String>>, Option<String>) {
3004        let saved = (
3005            self.expected_lambda_param_types.clone(),
3006            self.forced_lambda_ret.clone(),
3007        );
3008        if !matches!(tail.kind, NodeKind::Lambda { .. }) {
3009            return saved;
3010        }
3011        if let Some(node) = &self.current_fn_ret_type_node {
3012            if let Some((param_tys, ret_ty)) = self.fn_type_go_signature(node) {
3013                self.expected_lambda_param_types = Some(param_tys);
3014                self.forced_lambda_ret = Some(ret_ty);
3015            }
3016        }
3017        saved
3018    }
3019
3020    /// If `node` is a `Result[T, E]` type expression, return its `(T, E)` inner
3021    /// type nodes. The *node*-returning analogue of
3022    /// [`Self::result_elem_go_types`], used to peel a Result layer while
3023    /// descending a nested constructor pattern. Sees through a
3024    /// `type X = Result[T, E]` alias.
3025    fn result_inner_type_nodes<'a>(
3026        &'a self,
3027        node: &'a AIRNode,
3028    ) -> Option<(&'a AIRNode, Option<&'a AIRNode>)> {
3029        if let Some(target) = self.resolve_type_alias(node) {
3030            return self.result_inner_type_nodes(target);
3031        }
3032        if let NodeKind::TypeNamed { path, args } = &node.kind {
3033            if path.segments.last().is_some_and(|s| s.name == "Result") {
3034                return args.first().map(|ok| (ok, args.get(1)));
3035            }
3036        }
3037        None
3038    }
3039
3040    /// The `(K, V)` Go types of a `Map` *value* expression used as the receiver
3041    /// of a built-in map method. Recovered from a declared `Map[K, V]`
3042    /// identifier (via [`Self::var_map_kv`]) or a homogeneously-typed map
3043    /// literal. `None` ⇒ the caller falls back to `interface{}` (never a wrong
3044    /// type).
3045    /// The Go type of a compose-desugar lambda's sole parameter.
3046    ///
3047    /// `f >> g` lowers (in shared AIR) to `(__compose_x) => g(f(__compose_x))`
3048    /// with an *untyped* `__compose_x`. The composed value's input type is the
3049    /// input type of the inner function `f`, so recover `f`'s first declared
3050    /// parameter type (via [`Self::fn_param_types`]) and render it as Go. Returns
3051    /// `None` for any non-compose lambda, or when `f`'s param type can't be
3052    /// resolved (then the param stays `interface{}`, never a wrong type).
3053    fn compose_lambda_param_go_type(&self, params: &[AIRNode], body: &AIRNode) -> Option<String> {
3054        // Exactly one param, a `BindPat` (the synthetic `__compose_x`).
3055        let [param] = params else {
3056            return None;
3057        };
3058        let NodeKind::Param {
3059            pattern, ty: None, ..
3060        } = &param.kind
3061        else {
3062            return None;
3063        };
3064        let NodeKind::BindPat { name, .. } = &pattern.kind else {
3065            return None;
3066        };
3067        if name.name != "__compose_x" {
3068            return None;
3069        }
3070        // Body is `g(f(__compose_x))`; reach the inner call `f(__compose_x)`.
3071        let NodeKind::Call {
3072            args: outer_args, ..
3073        } = &body.kind
3074        else {
3075            return None;
3076        };
3077        let inner = &outer_args.first()?.value;
3078        let NodeKind::Call { callee: f, .. } = &inner.kind else {
3079            return None;
3080        };
3081        self.compose_input_go_type(f)
3082    }
3083
3084    /// The Go type of the *input* a composed callee `f` accepts — used to type a
3085    /// `>>`-compose lambda's synthetic `__compose_x` parameter.
3086    ///
3087    /// A named function (`Identifier`) yields its first declared parameter type.
3088    /// A *nested* compose (chained `f >> g >> h` desugars to a `(__compose_x) =>
3089    /// h(g(f(__compose_x)))` whose innermost callee `f` is itself a compose
3090    /// lambda) recurses through that lambda's own desugared shape to the
3091    /// innermost named function. Without the recursion an `f >> g >> h` chain's
3092    /// *outer* compose param fell back to `interface{}` on Go (only the innermost
3093    /// compose's param was typed), so passing `composeX` into the typed inner
3094    /// closure needed a type assertion Go rejected (Q-nested-compose-jstsgo, Go
3095    /// portion). Mirrors py's `emit_callee`/rust's `emit_callee_rs` parens
3096    /// strategy at the *typing* level.
3097    fn compose_input_go_type(&self, callee: &AIRNode) -> Option<String> {
3098        match &callee.kind {
3099            NodeKind::Identifier { name } => {
3100                let first_param = self.fn_param_types.get(&name.name)?.first()?.as_ref()?;
3101                Some(self.type_to_go(first_param))
3102            }
3103            // A nested compose lambda (`(__compose_x) => g(f(__compose_x))`): its
3104            // own input type is whatever its innermost composed callee `f`
3105            // accepts. Recurse through the same desugared shape.
3106            NodeKind::Lambda { params, body } => self.compose_lambda_param_go_type(params, body),
3107            _ => None,
3108        }
3109    }
3110
3111    fn map_receiver_kv_go_types(&self, recv: &AIRNode) -> Option<(String, String)> {
3112        match &recv.kind {
3113            NodeKind::Identifier { name } => {
3114                self.var_map_kv.get(&go_value_ident(&name.name)).cloned()
3115            }
3116            NodeKind::MapLiteral { entries } => {
3117                let keys: Vec<&AIRNode> = entries.iter().map(|e| &e.key).collect();
3118                let vals: Vec<&AIRNode> = entries.iter().map(|e| &e.value).collect();
3119                match (
3120                    self.infer_homogeneous_elem_type_refs(&keys),
3121                    self.infer_homogeneous_elem_type_refs(&vals),
3122                ) {
3123                    (Some(k), Some(v)) => Some((k, v)),
3124                    _ => None,
3125                }
3126            }
3127            // A `self.field` map receiver inside an impl method: the field's
3128            // `Map[K, V]` types are recorded per record (mirrors the `List`
3129            // case in `list_receiver_elem_go_type`).
3130            NodeKind::FieldAccess { object, field } if matches!(&object.kind, NodeKind::Identifier { name } if name.name == "self") =>
3131            {
3132                let record = self.current_self_record.as_ref()?;
3133                self.record_field_map_kv
3134                    .get(record)
3135                    .and_then(|m| m.get(&field.name))
3136                    .cloned()
3137            }
3138            // A `value.field` map receiver where `value` is a variable of a known
3139            // record type (`report.by_category.get(k)` for `report: Report`,
3140            // `record Report { by_category: Map[String, Float] }`). The variable's
3141            // Go type names the record; the field's recorded `Map[K, V]` types
3142            // give the closure's `map[K]V` rather than the erased
3143            // `map[interface{}]interface{}` Go rejects against the concrete field.
3144            NodeKind::FieldAccess { object, field } => {
3145                let NodeKind::Identifier { name } = &object.kind else {
3146                    return None;
3147                };
3148                let obj_go_ty = self.var_go_type.get(&go_value_ident(&name.name))?;
3149                let record = Self::go_type_record_head(obj_go_ty);
3150                self.record_field_map_kv
3151                    .get(record)
3152                    .and_then(|m| m.get(&field.name))
3153                    .cloned()
3154            }
3155            _ => None,
3156        }
3157    }
3158
3159    /// The Go element type of a `List` *value* expression, so an untyped binding
3160    /// to it (`let updated = items.map(..)`, `let evens = xs.filter(..)`) records
3161    /// its element type — letting a *chained* combinator on the binding
3162    /// (`updated.map((it) => it.title)`) type its closure param and a later use as
3163    /// a typed call argument keep `[]T` rather than erasing to `[]interface{}`.
3164    ///
3165    /// Handles a homogeneous list literal directly, and the closure-taking
3166    /// combinators `filter`/`map`/`flat_map` whose *receiver* element type is
3167    /// recoverable: `filter` preserves the element; `map` yields the closure's
3168    /// inferred return type (with the receiver element pinned as the closure
3169    /// param type); `flat_map` yields that return type's slice element. `None`
3170    /// when the element can't be recovered (the caller leaves the binding untyped
3171    /// — never a wrong type).
3172    fn value_list_elem_go_type(&mut self, value: &AIRNode) -> Option<String> {
3173        // A homogeneous list literal / typed `List[T]` identifier directly.
3174        if let Some(slice) = self.infer_go_expr_type(value) {
3175            if let Some(elem) = slice.strip_prefix("[]") {
3176                return Some(elem.to_string());
3177            }
3178        }
3179        // A builtin String-method call returning a concretely-typed list
3180        // (`s.split(..)` → `[]string`): the binding's element is known without
3181        // any combinator analysis (Q-go-split-combinator-typing — lets a `let
3182        // raw = s.split(..)` record `string` so a chain on the binding types).
3183        if let Some(elem) = Self::string_list_builtin_elem(value) {
3184            return Some(elem);
3185        }
3186        // A list combinator (`xs.map(..)`, `xs.filter(..)`) reaches codegen as the
3187        // desugared `Call(FieldAccess(xs, "map"), [cb])`, not a `MethodCall`;
3188        // recognise it through the shared desugar resolver.
3189        let NodeKind::Call { callee, args, .. } = &value.kind else {
3190            return None;
3191        };
3192        let (recv, method, rest) =
3193            crate::generator::desugared_list_functional_method(value, callee, args)?;
3194        // The cheap `&self` receiver resolver covers bindings/literals and the
3195        // element-preserving chained combinators; recurse through this *value*
3196        // resolver otherwise so a `map`/`flat_map` link in the chain (whose
3197        // element is its closure's return type) doesn't sever element recovery
3198        // for everything chained after it (`split(..).map(..).filter(..)`,
3199        // Q-go-split-combinator-typing).
3200        let recv_elem = self
3201            .list_receiver_elem_go_type(recv)
3202            .or_else(|| self.value_list_elem_go_type(recv))?;
3203        match method {
3204            "filter" => Some(recv_elem),
3205            "map" | "flat_map" => {
3206                let cb = rest.first()?;
3207                let NodeKind::Lambda { params, body } = &cb.value.kind else {
3208                    return None;
3209                };
3210                let saved = self.enter_param_go_types_with_expected(params, Some(&[recv_elem]));
3211                let ret = self.infer_block_tail_type(body);
3212                self.var_go_type = saved;
3213                let ret = ret?;
3214                if method == "flat_map" {
3215                    ret.strip_prefix("[]").map(str::to_string)
3216                } else {
3217                    Some(ret)
3218                }
3219            }
3220            _ => None,
3221        }
3222    }
3223
3224    /// The element Go type of a `Set` *value* expression used as the receiver of
3225    /// a built-in set method. Recovered from a declared `Set[E]` identifier (via
3226    /// [`Self::var_set_elem`]) or a homogeneously-typed set literal. `None` ⇒
3227    /// `interface{}` fallback.
3228    fn set_receiver_elem_go_type(&self, recv: &AIRNode) -> Option<String> {
3229        match &recv.kind {
3230            NodeKind::Identifier { name } => {
3231                self.var_set_elem.get(&go_value_ident(&name.name)).cloned()
3232            }
3233            NodeKind::SetLiteral { elems } => self.infer_homogeneous_elem_type(elems),
3234            _ => None,
3235        }
3236    }
3237
3238    /// Infer the `(K, V)` Go types of a `Map`-typed *value* expression — a map
3239    /// literal, a known `Map` identifier, or a `Map` built-in method that
3240    /// returns the receiver map (`set`/`delete`/`merge`/`filter`). Lets an
3241    /// untyped `let m2 = base.set(k, v)` propagate `base`'s key/value types onto
3242    /// `m2` so a subsequent `m2.get(k)` closure is well-typed. `None` ⇒
3243    /// `interface{}` fallback.
3244    fn value_map_kv_go_types(&self, value: &AIRNode) -> Option<(String, String)> {
3245        if let Some(kv) = self.map_receiver_kv_go_types(value) {
3246            return Some(kv);
3247        }
3248        if let NodeKind::Call { callee, args, .. } = &value.kind {
3249            if let Some((recv, method, _)) =
3250                crate::generator::desugared_map_method(value, callee, args)
3251            {
3252                if matches!(method, "set" | "delete" | "merge" | "filter") {
3253                    return self.value_map_kv_go_types(recv);
3254                }
3255            }
3256        }
3257        None
3258    }
3259
3260    /// Infer the element Go type of a `Set`-typed *value* expression — a set
3261    /// literal, a known `Set` identifier, or a `Set` built-in returning the
3262    /// receiver set (`add`/`remove`/`union`/`intersection`/`difference`/
3263    /// `filter`/`map`). The `Set` analogue of [`Self::value_map_kv_go_types`].
3264    fn value_set_elem_go_type(&self, value: &AIRNode) -> Option<String> {
3265        if let Some(elem) = self.set_receiver_elem_go_type(value) {
3266            return Some(elem);
3267        }
3268        if let NodeKind::Call { callee, args, .. } = &value.kind {
3269            if let Some((recv, method, _)) =
3270                crate::generator::desugared_set_method(value, callee, args)
3271            {
3272                if matches!(
3273                    method,
3274                    "add" | "remove" | "union" | "intersection" | "difference" | "filter" | "map"
3275                ) {
3276                    return self.value_set_elem_go_type(recv);
3277                }
3278            }
3279        }
3280        None
3281    }
3282
3283    /// Infer the concrete generic-record instantiation a *value* expression
3284    /// produces — `("ListIterator", ["int64"])` for `list_iter([1, 2, 3])` or
3285    /// `bag.iter()`. Resolved for: a call to a generic fn whose return type is a
3286    /// generic record ([`Self::infer_go_expr_type`]'s `Call` arm), and a method
3287    /// call (direct or the desugared `Call(FieldAccess(recv, m), [recv, ..])`
3288    /// shape) whose method has a recorded concrete record return
3289    /// ([`Self::method_ret_record_args`]). Used to record an untyped binding's
3290    /// record args so a later `match binding.next() { Some(x) => ... }` asserts
3291    /// the payload concretely. `None` when not structurally determinable.
3292    fn value_record_type_args(&self, value: &AIRNode) -> Option<(String, Vec<String>)> {
3293        // A method whose declared return is a concrete generic record.
3294        match &value.kind {
3295            NodeKind::MethodCall { method, .. } => {
3296                if let Some(args) = self.method_ret_record_args.get(&method.name) {
3297                    return Some(args.clone());
3298                }
3299            }
3300            NodeKind::Call { callee, args, .. } => {
3301                // The AIR also lowers `recv.m(rest)` to `Call(FieldAccess(recv,
3302                // m), [recv, ...])`.
3303                if let Some((_recv, method, _)) =
3304                    crate::generator::desugared_self_call(callee, args)
3305                {
3306                    if let Some(ra) = self.method_ret_record_args.get(&method.name) {
3307                        return Some(ra.clone());
3308                    }
3309                }
3310            }
3311            _ => {}
3312        }
3313        // A free generic-fn call resolving to a concrete record return
3314        // (`list_iter([...])` → `ListIterator[int64]`): parse the rendered type.
3315        let go_ty = self.infer_go_expr_type(value)?;
3316        let open = go_ty.find('[')?;
3317        if !go_ty.ends_with(']') {
3318            return None;
3319        }
3320        let base = go_ty[..open].to_string();
3321        if self.generic_decls.get(&base).is_none_or(|p| p.is_empty()) {
3322            return None;
3323        }
3324        let arg_str = &go_ty[open + 1..go_ty.len() - 1];
3325        let args = Self::split_top_level_commas(arg_str);
3326        if args.is_empty() {
3327            return None;
3328        }
3329        Some((base, args))
3330    }
3331
3332    /// Infer the `(ok, err)` Go payload types a `Result`-typed *value* expression
3333    /// produces. The `Result` analogue of [`Self::value_map_kv_go_types`]:
3334    /// resolves a bare `Ok(v)` / `Err(e)` constructor's present arm from the
3335    /// payload expression (the absent arm stays `interface{}`), and a call to a
3336    /// function whose declared return is a `Result` via the existing
3337    /// [`Self::scrutinee_result_elems`] (fn-return / variable maps). Used to
3338    /// record an *untyped* binding's `(ok, err)` into [`Self::var_result_elem`]
3339    /// (`step1 := eval(...)`, with no `: Result[..]` annotation), so a later
3340    /// `match step1 { Ok(v) => ...; Err(e) => ... }` type-asserts the
3341    /// `interface{}` payload concretely rather than binding it bare (which leaves
3342    /// `v` as `interface{}` and fails a later use expecting the concrete type).
3343    fn value_result_elem_go_types(&self, value: &AIRNode) -> Option<(String, String)> {
3344        if let NodeKind::Call { callee, args, .. } = &value.kind {
3345            if let NodeKind::Identifier { name } = &callee.kind {
3346                match name.name.as_str() {
3347                    "Ok" => {
3348                        let ok = args
3349                            .first()
3350                            .and_then(|a| self.infer_go_expr_type(&a.value))
3351                            .unwrap_or_else(|| "interface{}".to_string());
3352                        return Some((ok, "interface{}".to_string()));
3353                    }
3354                    "Err" => {
3355                        let err = args
3356                            .first()
3357                            .and_then(|a| self.infer_go_expr_type(&a.value))
3358                            .unwrap_or_else(|| "interface{}".to_string());
3359                        return Some(("interface{}".to_string(), err));
3360                    }
3361                    _ => {}
3362                }
3363            }
3364        }
3365        self.scrutinee_result_elems(value)
3366    }
3367
3368    /// For a `List[T]` / `Set[T]` / `Map[K, V]` type expression, the declared
3369    /// Go element types as `(elem_or_key, value)`: `List`/`Set` yield
3370    /// `(T, None)`; `Map` yields `(K, Some(V))`. A missing type arg defaults to
3371    /// `interface{}`. `None` for any non-collection type. Used to set
3372    /// [`Self::expected_collection_elem`] so a literal in a typed binding adopts
3373    /// the declared element type(s).
3374    /// If `node` is a *generic record* instantiation (`ListIter[Int]`), return
3375    /// its base name and the Go-rendered concrete type-args (`("ListIter",
3376    /// ["int64"])`). `None` for a non-record type, a non-generic record, or a
3377    /// record with no type-args. Used to record [`Self::var_record_type_args`]
3378    /// so a method-call scrutinee's generic `Optional[T]` payload can be resolved
3379    /// to the concrete instantiation at the call site.
3380    fn record_type_args(&self, node: &AIRNode) -> Option<(String, Vec<String>)> {
3381        let NodeKind::TypeNamed { path, args } = &node.kind else {
3382            return None;
3383        };
3384        if args.is_empty() {
3385            return None;
3386        }
3387        let base = path.segments.last().map(|s| s.name.clone())?;
3388        // Only generic records (those with a declared param list) qualify; this
3389        // keeps the map free of `List`/`Map`/etc. and other non-record applies.
3390        let params = self.generic_decls.get(&base)?;
3391        if params.is_empty() {
3392            return None;
3393        }
3394        let arg_strs: Vec<String> = args.iter().map(|a| self.type_to_go(a)).collect();
3395        Some((base, arg_strs))
3396    }
3397
3398    fn collection_elem_go_types(&self, node: &AIRNode) -> Option<(String, Option<String>)> {
3399        let NodeKind::TypeNamed { path, args } = &node.kind else {
3400            return None;
3401        };
3402        let name = path.segments.last().map(|s| s.name.as_str())?;
3403        let arg = |i: usize| {
3404            args.get(i)
3405                .map_or_else(|| "interface{}".to_string(), |a| self.type_to_go(a))
3406        };
3407        match name {
3408            "List" | "Set" => Some((arg(0), None)),
3409            "Map" => Some((arg(0), Some(arg(1)))),
3410            _ => None,
3411        }
3412    }
3413
3414    /// The Go element type a `for x in <iterable>` loop binds, when
3415    /// structurally recoverable:
3416    /// - an identifier whose declared `List[T]` element type is in
3417    ///   [`Self::var_list_elem`] (a typed `let` / parameter),
3418    /// - a list literal whose elements infer to one homogeneous Go type,
3419    /// - a range (`a..b` / `a..=b`), which yields `int64`.
3420    ///
3421    /// Returns `None` otherwise; the loop variable is then left out of the type
3422    /// scope and inference falls back to `interface{}` — never a wrong type.
3423    fn for_loop_elem_go_type(&self, iterable: &AIRNode) -> Option<String> {
3424        match &iterable.kind {
3425            NodeKind::Identifier { name } => {
3426                self.var_list_elem.get(&go_value_ident(&name.name)).cloned()
3427            }
3428            NodeKind::ListLiteral { elems } => self.infer_homogeneous_elem_type(elems),
3429            NodeKind::Range { .. } => Some("int64".to_string()),
3430            // `for p in s.split(",")`: the builtin lowers to `strings.Split`,
3431            // a concrete `[]string` (Q-go-split-combinator-typing).
3432            _ => Self::string_list_builtin_elem(iterable),
3433        }
3434    }
3435
3436    /// The concrete Go type a builtin *String-method* call lowers to, keyed off
3437    /// the same checker receiver-kind annotation [`Self::try_emit_string_method`]
3438    /// dispatches on (so a user method named `trim` on a non-String receiver is
3439    /// never mistaken for the builtin). This table mirrors that emitter's
3440    /// lowerings exactly: `split` → `strings.Split` (`[]string`), the
3441    /// string-transforming methods → `string`, the length queries → `int64`, the
3442    /// predicates → `bool`, and the optional-returning lookups → the
3443    /// `__bockOption` runtime struct. Previously codegen had no return type for
3444    /// any of these, so a combinator chained onto `split()` — or a `.map((p) =>
3445    /// p.trim())` link feeding a chain — erased to `interface{}`, which Go
3446    /// rejects against the lowering's concrete types
3447    /// (Q-go-split-combinator-typing — the builtin-method sibling of the #256
3448    /// chained-combinator fix).
3449    fn string_builtin_return_go_type(
3450        node: &AIRNode,
3451        callee: &AIRNode,
3452        args: &[bock_air::AirArg],
3453    ) -> Option<String> {
3454        if crate::generator::primitive_recv_kind(node) != Some("String") {
3455            return None;
3456        }
3457        let (_, field, _) = crate::generator::desugared_self_call(callee, args)?;
3458        let ty = match field.name.as_str() {
3459            "to_upper" | "to_lower" | "trim" | "trim_start" | "trim_end" | "reverse" | "repeat"
3460            | "replace" | "slice" | "substring" | "to_string" | "display" => "string",
3461            "split" => "[]string",
3462            "len" | "length" | "count" | "byte_len" => "int64",
3463            "is_empty" | "contains" | "starts_with" | "ends_with" => "bool",
3464            "char_at" | "index_of" => "__bockOption",
3465            _ => return None,
3466        };
3467        Some(ty.to_string())
3468    }
3469
3470    /// The Go element type of a builtin *String-method* call returning a
3471    /// `List` — today only `split`, whose lowering is a concrete `[]string`
3472    /// (Q-go-split-combinator-typing). The slice-element view of
3473    /// [`Self::string_builtin_return_go_type`] for the list-element resolvers.
3474    fn string_list_builtin_elem(recv: &AIRNode) -> Option<String> {
3475        let NodeKind::Call { callee, args, .. } = &recv.kind else {
3476            return None;
3477        };
3478        Self::string_builtin_return_go_type(recv, callee, args)?
3479            .strip_prefix("[]")
3480            .map(str::to_string)
3481    }
3482
3483    /// The Go slice element type of a `List` *value* expression used as the
3484    /// receiver of a built-in list method (`get`/`concat`/…). The list-method
3485    /// closures take a `[]<elem>` parameter that must match the receiver's now
3486    /// concretely-typed slice. Recovered from a declared `List[T]` identifier
3487    /// (via [`Self::var_list_elem`]) or a homogeneously-typed list literal;
3488    /// `None` otherwise, in which case the receiver is `[]interface{}` and the
3489    /// `interface{}` element default matches.
3490    fn list_receiver_elem_go_type(&self, recv: &AIRNode) -> Option<String> {
3491        match &recv.kind {
3492            NodeKind::Identifier { name } => {
3493                let key = go_value_ident(&name.name);
3494                // A `List[T]` binding records its element type directly. Fall back
3495                // to the variable's full Go type (`var_go_type`) when the
3496                // dedicated list-element map has no entry — a typed *lambda
3497                // parameter* (`(data) => data.filter(..)` for a `Fn(List[T]) ->
3498                // ..` return type) is recorded only there as `[]T`, so peeling the
3499                // `[]` prefix recovers the element. Without this the chained
3500                // `.filter`/`.map` closure stayed `func(x interface{})` and its
3501                // `[]interface{}` result did not satisfy the typed `Fn` return.
3502                self.var_list_elem.get(&key).cloned().or_else(|| {
3503                    self.var_go_type
3504                        .get(&key)
3505                        .and_then(|t| t.strip_prefix("[]"))
3506                        .map(str::to_string)
3507                })
3508            }
3509            NodeKind::ListLiteral { elems } => self.infer_homogeneous_elem_type(elems),
3510            // A *chained* combinator whose receiver is itself a closure-taking
3511            // list method (`numbers.filter(..).map(..)`): the outer method's
3512            // receiver is the desugared `Call(FieldAccess(numbers, "filter"),
3513            // [numbers, cb])`. `filter`/`find` preserve the element type, so the
3514            // element is recoverable from the inner receiver without inferring a
3515            // closure return type (which would need `&mut self`). This keeps a
3516            // `.filter(..).map(..)` chain's outer `.map` closure typed
3517            // `func(n int64)` and its result `[]int64` rather than the erased
3518            // `interface{}`/`[]interface{}` that Go rejects. `map`/`flat_map`
3519            // receivers (whose element is the closure's return type) are recovered
3520            // by the `&mut self` fallback in `try_emit_list_functional_method`.
3521            NodeKind::Call { callee, args, .. } => {
3522                if let Some((inner_recv, method, _)) =
3523                    crate::generator::desugared_list_functional_method(recv, callee, args)
3524                {
3525                    match method {
3526                        "filter" | "find" => self.list_receiver_elem_go_type(inner_recv),
3527                        _ => None,
3528                    }
3529                } else {
3530                    // Not a list combinator: a builtin String-method call that
3531                    // returns a concretely-typed list (`s.split(..)` →
3532                    // `strings.Split` → `[]string`) still carries a known
3533                    // element (Q-go-split-combinator-typing).
3534                    Self::string_list_builtin_elem(recv)
3535                }
3536            }
3537            // A `self.field` list receiver inside an impl method (`self.xs.get(i)`
3538            // in `record ListIter[T] { xs: List[T] }`): the field's `List[...]`
3539            // element type is recorded per record. `T` is in scope on the
3540            // method receiver, so the closure correctly takes `[]T`.
3541            NodeKind::FieldAccess { object, field } if matches!(&object.kind, NodeKind::Identifier { name } if name.name == "self") =>
3542            {
3543                let record = self.current_self_record.as_ref()?;
3544                self.record_field_list_elem
3545                    .get(record)
3546                    .and_then(|m| m.get(&field.name))
3547                    .cloned()
3548            }
3549            // A `value.field` list receiver where `value` is a variable of a known
3550            // record type (`b.items.get(i)` for `b: Box[T]`, `record Box[T] {
3551            // items: List[T] }`). The variable's Go type (`Box[T]`) names the
3552            // record; the field's recorded `List[...]` element type (`T`, in scope
3553            // as the enclosing generic fn's type param) gives the closure's `[]T`
3554            // element rather than the `[]interface{}` default — which a `[]T`
3555            // field-access argument does not satisfy under Go's type rules. (GAP-A:
3556            // a generic free fn reading `b.items.get(i)` previously emitted the
3557            // `.get` closure with a `[]interface{}` parameter and bound the `Some`
3558            // payload as `interface{}`, both rejected against `[]T`/`T`.)
3559            NodeKind::FieldAccess { object, field } => {
3560                let NodeKind::Identifier { name } = &object.kind else {
3561                    return None;
3562                };
3563                let obj_go_ty = self.var_go_type.get(&go_value_ident(&name.name))?;
3564                let record = Self::go_type_record_head(obj_go_ty);
3565                self.record_field_list_elem
3566                    .get(record)
3567                    .and_then(|m| m.get(&field.name))
3568                    .cloned()
3569            }
3570            _ => None,
3571        }
3572    }
3573
3574    /// The record/type head of a Go type rendering: the identifier before any
3575    /// generic `[...]` arg list (`Box[T]` → `Box`, `Box[int64]` → `Box`, `Box` →
3576    /// `Box`). Used to key [`Self::record_field_list_elem`] from a variable's
3577    /// recorded Go type when resolving a `value.field` list receiver.
3578    fn go_type_record_head(go_ty: &str) -> &str {
3579        go_ty.split('[').next().unwrap_or(go_ty).trim()
3580    }
3581
3582    /// Parse the per-field Go types out of a tuple struct rendering
3583    /// (`struct{ Field0 int64; Field1 string }` → `["int64", "string"]`). The
3584    /// inverse of `type_to_go`'s `TypeTuple` arm. Returns an empty vec for any
3585    /// non-tuple-struct string. Used to pin a tuple literal's field types from a
3586    /// declared tuple return/binding type when element inference falls short.
3587    fn parse_tuple_struct_field_types(go_ty: &str) -> Vec<String> {
3588        let inner = match go_ty
3589            .trim()
3590            .strip_prefix("struct{")
3591            .and_then(|s| s.strip_suffix('}'))
3592        {
3593            Some(s) => s.trim(),
3594            None => return Vec::new(),
3595        };
3596        let mut out = Vec::new();
3597        for field in inner.split(';') {
3598            let field = field.trim();
3599            if field.is_empty() {
3600                continue;
3601            }
3602            // Each field is `Field<N> <ty>`; the type is everything after the
3603            // first whitespace-separated token.
3604            match field.split_once(char::is_whitespace) {
3605                Some((name, ty)) if name.starts_with("Field") => out.push(ty.trim().to_string()),
3606                _ => return Vec::new(),
3607            }
3608        }
3609        out
3610    }
3611
3612    /// True when `node` is (or contains, in operand position) an identifier
3613    /// whose Go type is not in `scope` — i.e. an `interface{}`-typed value an
3614    /// arithmetic operation cannot soundly operate on. Used to keep arithmetic
3615    /// type-inference conservative (untyped lambda params stay `interface{}`).
3616    fn has_unresolved_operand(node: &AIRNode, scope: &HashMap<String, String>) -> bool {
3617        match &node.kind {
3618            NodeKind::Identifier { name } => !scope.contains_key(&go_value_ident(&name.name)),
3619            NodeKind::UnaryOp { operand, .. } => Self::has_unresolved_operand(operand, scope),
3620            NodeKind::BinaryOp { left, right, .. } => {
3621                Self::has_unresolved_operand(left, scope)
3622                    || Self::has_unresolved_operand(right, scope)
3623            }
3624            _ => false,
3625        }
3626    }
3627
3628    /// Best-effort structural inference of an expression's Go type. Reaches the
3629    /// cases needed to (a) instantiate a generic struct construction
3630    /// (`Box[int64]{...}`) and (b) give a lambda a concrete return type rather
3631    /// than `interface{}`. Handles literals, in-scope identifiers (via
3632    /// [`Self::var_go_type`]), arithmetic/comparison binary ops, and unary ops.
3633    /// Returns `None` when the type can't be determined structurally — callers
3634    /// fall back to `any`/`interface{}`, never a wrong type.
3635    /// Structurally unify a generic-param *pattern* go-type (a `type_to_go`
3636    /// rendering that still names the type params, e.g. `ListIterator[T]` or
3637    /// `[]T`) against a *concrete* go-type string, recording each param's
3638    /// concrete binding into `bindings`. A bare pattern that is exactly a
3639    /// generic-param name binds that param to the whole concrete string;
3640    /// otherwise the two must share a structural skeleton (same brackets/commas
3641    /// in the same places) and the unification recurses into the differing
3642    /// segments. Conservative: a structural mismatch simply records nothing
3643    /// (the caller then leaves the lambda param untyped — never wrong, only
3644    /// loose). Only the `[`/`]`/`,` skeleton is parsed; this covers the generic
3645    /// container / iterator shapes the combinators use.
3646    fn unify_go_pattern(
3647        pattern: &str,
3648        concrete: &str,
3649        gp_names: &[String],
3650        bindings: &mut HashMap<String, String>,
3651    ) {
3652        let pat = pattern.trim();
3653        let con = concrete.trim();
3654        // A bare param name binds to the entire concrete type.
3655        if gp_names.iter().any(|g| g == pat) {
3656            bindings
3657                .entry(pat.to_string())
3658                .or_insert_with(|| con.to_string());
3659            return;
3660        }
3661        // Split each into (head, bracketed-args) on the first top-level `[`.
3662        let split = |s: &str| -> Option<(String, String)> {
3663            let open = s.find('[')?;
3664            if !s.ends_with(']') {
3665                return None;
3666            }
3667            Some((s[..open].to_string(), s[open + 1..s.len() - 1].to_string()))
3668        };
3669        // A slice prefix `[]elem` — split into the `[]` marker and the element.
3670        if let (Some(pe), Some(ce)) = (pat.strip_prefix("[]"), con.strip_prefix("[]")) {
3671            Self::unify_go_pattern(pe, ce, gp_names, bindings);
3672            return;
3673        }
3674        match (split(pat), split(con)) {
3675            (Some((ph, pa)), Some((ch, ca))) if ph == ch => {
3676                let p_args = Self::split_top_level_commas(&pa);
3677                let c_args = Self::split_top_level_commas(&ca);
3678                if p_args.len() == c_args.len() {
3679                    for (pp, cc) in p_args.iter().zip(c_args.iter()) {
3680                        Self::unify_go_pattern(pp, cc, gp_names, bindings);
3681                    }
3682                }
3683            }
3684            _ => {}
3685        }
3686    }
3687
3688    /// Split a go-type-arg list on top-level commas (commas not nested inside a
3689    /// `[...]`). `int64, []string` → `["int64", "[]string"]`.
3690    fn split_top_level_commas(s: &str) -> Vec<String> {
3691        let mut out = Vec::new();
3692        let mut depth = 0i32;
3693        let mut start = 0usize;
3694        for (i, ch) in s.char_indices() {
3695            match ch {
3696                '[' | '(' => depth += 1,
3697                ']' | ')' => depth -= 1,
3698                ',' if depth == 0 => {
3699                    out.push(s[start..i].trim().to_string());
3700                    start = i + 1;
3701                }
3702                _ => {}
3703            }
3704        }
3705        let last = s[start..].trim();
3706        if !last.is_empty() {
3707            out.push(last.to_string());
3708        }
3709        out
3710    }
3711
3712    /// Bind a generic fn's type params to concrete go-types from its call's
3713    /// *non-lambda* arguments (a lambda argument is what we are specialising, so
3714    /// it can't drive the binding). Each argument whose Go type infers is
3715    /// unified against the matching declared param type. Returns the
3716    /// `param-name → go-type` bindings discovered.
3717    fn bind_fn_type_params(
3718        &self,
3719        gp_names: &[String],
3720        param_tys: &[Option<AIRNode>],
3721        args: &[bock_air::AirArg],
3722    ) -> HashMap<String, String> {
3723        let mut bindings: HashMap<String, String> = HashMap::new();
3724        for (i, arg) in args.iter().enumerate() {
3725            if matches!(arg.value.kind, NodeKind::Lambda { .. }) {
3726                continue;
3727            }
3728            let Some(pty) = param_tys.get(i).and_then(|p| p.as_ref()) else {
3729                continue;
3730            };
3731            let Some(arg_go) = self.infer_go_expr_type(&arg.value) else {
3732                continue;
3733            };
3734            let pattern = self.type_to_go(pty);
3735            Self::unify_go_pattern(&pattern, &arg_go, gp_names, &mut bindings);
3736        }
3737        bindings
3738    }
3739
3740    /// Substitute the `param-name → go-type` bindings into a `Fn(...)` parameter
3741    /// type, returning the concrete Go parameter types (`[int64]` for
3742    /// `Fn(T) -> Bool` with `T → int64`). `None` when the param type is not a
3743    /// function type or a needed binding is missing (caller leaves the lambda
3744    /// param untyped).
3745    fn specialise_lambda_param_types(
3746        &self,
3747        fn_param_ty: &AIRNode,
3748        gp_names: &[String],
3749        bindings: &HashMap<String, String>,
3750    ) -> Option<Vec<String>> {
3751        // A callee param declared via a `type` alias to a function type
3752        // (`type Predicate = Fn(Int) -> Bool`) is a `TypeNamed`; see through it
3753        // to the underlying `TypeFunction` so the lambda argument still gets its
3754        // param types (`func(x int64) bool`, not the erased `interface{}`).
3755        if let Some(target) = self.resolve_type_alias(fn_param_ty) {
3756            return self.specialise_lambda_param_types(target, gp_names, bindings);
3757        }
3758        let NodeKind::TypeFunction { params, .. } = &fn_param_ty.kind else {
3759            return None;
3760        };
3761        let mut out = Vec::with_capacity(params.len());
3762        for p in params {
3763            let rendered = self.type_to_go(p);
3764            // Substitute each bound param name token-for-token. The rendered form
3765            // names params verbatim (`T`, `[]T`), so a binding maps them.
3766            let resolved = if let Some(b) = bindings.get(rendered.trim()) {
3767                b.clone()
3768            } else {
3769                let mut r = rendered.clone();
3770                for g in gp_names {
3771                    if let Some(b) = bindings.get(g) {
3772                        r = Self::replace_type_token(&r, g, b);
3773                    }
3774                }
3775                // If any generic param token remains unbound, give up (untyped).
3776                if gp_names.iter().any(|g| Self::contains_type_token(&r, g)) {
3777                    return None;
3778                }
3779                r
3780            };
3781            out.push(resolved);
3782        }
3783        Some(out)
3784    }
3785
3786    /// Replace whole-identifier occurrences of `token` in a go-type string with
3787    /// `repl` (so `T` in `[]T` becomes the binding, without clobbering a `T`
3788    /// inside a longer identifier like `Tree`).
3789    fn replace_type_token(s: &str, token: &str, repl: &str) -> String {
3790        let bytes = s.as_bytes();
3791        let mut out = String::with_capacity(s.len());
3792        let mut i = 0;
3793        while i < s.len() {
3794            if s[i..].starts_with(token) {
3795                let before_ok = i == 0 || !Self::is_ident_byte(bytes[i - 1]);
3796                let after_idx = i + token.len();
3797                let after_ok = after_idx >= s.len() || !Self::is_ident_byte(bytes[after_idx]);
3798                if before_ok && after_ok {
3799                    out.push_str(repl);
3800                    i = after_idx;
3801                    continue;
3802                }
3803            }
3804            // Push one char (handle UTF-8 boundaries safely).
3805            let ch = s[i..].chars().next().unwrap_or(' ');
3806            out.push(ch);
3807            i += ch.len_utf8();
3808        }
3809        out
3810    }
3811
3812    /// True when `s` contains `token` as a whole identifier (used to detect an
3813    /// unbound generic param remaining after substitution).
3814    fn contains_type_token(s: &str, token: &str) -> bool {
3815        Self::replace_type_token(s, token, "\0") != *s
3816    }
3817
3818    fn is_ident_byte(b: u8) -> bool {
3819        b.is_ascii_alphanumeric() || b == b'_'
3820    }
3821
3822    /// The Go type-name a `RecordConstruct` lowers its struct literal to: the
3823    /// record/struct-variant name (`Item`, or `ShapeCircle` for a struct-variant
3824    /// construction). Mirrors the `type_name` computation in the `RecordConstruct`
3825    /// emission so [`Self::infer_go_expr_type`] can type a list literal of
3826    /// record-constructs (`[Item{...}, Item{...}]` → `[]Item`) instead of erasing
3827    /// it to `[]interface{}` (the GAP-A defect: `infer_go_expr_type` had no
3828    /// `RecordConstruct` arm, so the homogeneous-element inference failed and the
3829    /// `Box[T] { items: List[T] }` field literal became `[]interface{}{…}`, which
3830    /// `go build` rejects against the struct's `[]Item` field).
3831    fn record_construct_go_type_name(&self, path: &bock_ast::TypePath) -> String {
3832        if let Some(info) = self.user_variant_for_path(path) {
3833            let variant = path.segments.last().map_or("", |s| s.name.as_str());
3834            format!("{}{variant}", info.enum_name)
3835        } else {
3836            path.segments
3837                .iter()
3838                .map(|s| s.name.as_str())
3839                .collect::<Vec<_>>()
3840                .join(".")
3841        }
3842    }
3843
3844    /// Build a `param → concrete Go type` substitution for a record construction
3845    /// from the record's declared generic-param names and the resolved type-arg
3846    /// suffix (`"[Key]"`, `"[T]"`, `"[any]"`). A param with no positional arg
3847    /// (malformed/empty suffix) is omitted. Used to specialise a `List[T]` field
3848    /// literal's element type at the construction site.
3849    fn record_param_substitution(
3850        &self,
3851        record_name: &str,
3852        type_args: &str,
3853    ) -> HashMap<String, String> {
3854        let mut subst = HashMap::new();
3855        let Some(params) = self.record_generic_param_names.get(record_name) else {
3856            return subst;
3857        };
3858        let args = Self::split_type_arg_suffix(type_args);
3859        for (param, arg) in params.iter().zip(args.iter()) {
3860            subst.insert(param.clone(), arg.clone());
3861        }
3862        subst
3863    }
3864
3865    /// Split a Go type-argument suffix (`"[A, B[C]]"`) into its top-level args
3866    /// (`["A", "B[C]"]`), respecting bracket nesting so a nested instantiation is
3867    /// not split at its inner comma. Returns `[]` for an empty/absent suffix.
3868    fn split_type_arg_suffix(suffix: &str) -> Vec<String> {
3869        let inner = suffix
3870            .strip_prefix('[')
3871            .and_then(|s| s.strip_suffix(']'))
3872            .unwrap_or("");
3873        let mut args = Vec::new();
3874        let mut depth = 0usize;
3875        let mut cur = String::new();
3876        for ch in inner.chars() {
3877            match ch {
3878                '[' => {
3879                    depth += 1;
3880                    cur.push(ch);
3881                }
3882                ']' => {
3883                    depth = depth.saturating_sub(1);
3884                    cur.push(ch);
3885                }
3886                ',' if depth == 0 => {
3887                    args.push(cur.trim().to_string());
3888                    cur.clear();
3889                }
3890                _ => cur.push(ch),
3891            }
3892        }
3893        if !cur.trim().is_empty() {
3894            args.push(cur.trim().to_string());
3895        }
3896        args
3897    }
3898
3899    /// Substitute whole-token generic-param occurrences in a Go type string
3900    /// using `subst` (`"T"` with `{T: "Key"}` → `"Key"`; `"[]T"` is handled by
3901    /// the caller, which passes the bare element type). Only an exact match is
3902    /// substituted — composite element types are rare for record list fields, so
3903    /// this keeps the rewrite token-precise rather than risking a substring hit.
3904    fn apply_type_subst(elem: &str, subst: &HashMap<String, String>) -> String {
3905        subst.get(elem).cloned().unwrap_or_else(|| elem.to_string())
3906    }
3907
3908    /// Pre-scan a block's statements for declare-only temps (from the shared
3909    /// value-CF hoist) and record the Go type each `var __bock_cf_N T` needs.
3910    /// A declare-only `let` is always immediately followed by its relocated
3911    /// control-flow statement, whose branch values (`temp = <value>` assignments)
3912    /// determine the temp's type; infer it via [`Self::infer_assigned_temp_type`].
3913    /// Idempotent — re-records on each block entry, scoped to the temps it sees.
3914    fn seed_decl_only_types(&mut self, stmts: &[AIRNode]) {
3915        for (i, s) in stmts.iter().enumerate() {
3916            let NodeKind::LetBinding { pattern, .. } = &s.kind else {
3917                continue;
3918            };
3919            if !s.metadata.contains_key(crate::generator::DECL_ONLY_META) {
3920                continue;
3921            }
3922            let name = self.pattern_to_go_binding(pattern);
3923            // The relocated CF is the next statement; its value arms were
3924            // rewritten to `name = <value>` assignments, so infer the temp's Go
3925            // type from the assigned values. Falls back to `interface{}` (still
3926            // valid Go) when no assignment's value type is determinable.
3927            if let Some(next) = stmts.get(i + 1) {
3928                if let Some(ty) = self.infer_assigned_temp_type(next, &name) {
3929                    self.decl_only_types.insert(name, ty);
3930                }
3931            }
3932        }
3933    }
3934
3935    /// Infer the common Go type assigned to temp `name` anywhere within `node`
3936    /// (the relocated control-flow statement of a value-CF hoist). Scans every
3937    /// `name = <value>` assignment and unifies their value types; returns `None`
3938    /// when they disagree or none is determinable. Does not descend into nested
3939    /// functions/lambdas.
3940    fn infer_assigned_temp_type(&self, node: &AIRNode, name: &str) -> Option<String> {
3941        fn collect<'a>(node: &'a AIRNode, name: &str, out: &mut Vec<&'a AIRNode>) {
3942            match &node.kind {
3943                NodeKind::Assign { target, value, .. } => {
3944                    if matches!(&target.kind, NodeKind::Identifier { name: n } if go_value_ident(&n.name) == name)
3945                    {
3946                        out.push(value);
3947                    }
3948                }
3949                NodeKind::FnDecl { .. } | NodeKind::Lambda { .. } => {}
3950                NodeKind::Block { stmts, tail } => {
3951                    for s in stmts {
3952                        collect(s, name, out);
3953                    }
3954                    if let Some(t) = tail {
3955                        collect(t, name, out);
3956                    }
3957                }
3958                NodeKind::If {
3959                    then_block,
3960                    else_block,
3961                    ..
3962                } => {
3963                    collect(then_block, name, out);
3964                    if let Some(e) = else_block {
3965                        collect(e, name, out);
3966                    }
3967                }
3968                NodeKind::Match { arms, .. } => {
3969                    for arm in arms {
3970                        if let NodeKind::MatchArm { body, .. } = &arm.kind {
3971                            collect(body, name, out);
3972                        }
3973                    }
3974                }
3975                NodeKind::Loop { body } | NodeKind::While { body, .. } => {
3976                    collect(body, name, out);
3977                }
3978                _ => {}
3979            }
3980        }
3981        let mut values = Vec::new();
3982        collect(node, name, &mut values);
3983        // Unify only the values whose Go type is *determinable*. An assignment
3984        // whose value can't be typed here (e.g. a pattern-bound `v` not yet in
3985        // `var_go_type`) does not constrain — mirroring how `infer_branchy_expr_type`
3986        // skips value-less arms. This recovers `int64` from `bockCf0 = (0-1)` even
3987        // when a sibling `bockCf0 = v` is opaque, rather than collapsing to the
3988        // unassignable `interface{}`. If the determinable ones disagree, give up.
3989        let mut common: Option<String> = None;
3990        for v in values {
3991            let Some(ty) = self.infer_block_tail_type(v) else {
3992                continue;
3993            };
3994            match &common {
3995                Some(c) if *c != ty => return None,
3996                Some(_) => {}
3997                None => common = Some(ty),
3998            }
3999        }
4000        common
4001    }
4002
4003    /// Infer the Go value type produced by an `if`/`match` expression by
4004    /// inferring the type of each branch/arm's *tail* (value) and requiring them
4005    /// to agree. Used to type an untyped `let m = if (..) { Text } else { Image }`
4006    /// binding's IIFE: the inferred enum (`MessageType`) becomes the IIFE return
4007    /// type so a variant value is assignable, rather than the `interface{}` /
4008    /// enclosing-fn-return fallback. Returns `None` when any branch's type can't
4009    /// be inferred or the branches disagree (the caller then leaves the binding
4010    /// untyped, preserving the prior behavior — never a wrong type). Branches
4011    /// that *only* early-return (no value tail, e.g. a `return Err(..)` arm) are
4012    /// skipped: they exit the enclosing function rather than contributing a value.
4013    fn infer_branchy_expr_type(&self, node: &AIRNode) -> Option<String> {
4014        match &node.kind {
4015            NodeKind::If {
4016                then_block,
4017                else_block,
4018                ..
4019            } => {
4020                let then_ty = self.infer_block_tail_type(then_block);
4021                let else_ty = else_block
4022                    .as_deref()
4023                    .and_then(|e| self.infer_block_tail_type(e));
4024                match (then_ty, else_ty) {
4025                    (Some(a), Some(b)) if a == b => Some(a),
4026                    // One branch only early-returns (no value) — adopt the other.
4027                    (Some(a), None) => Some(a),
4028                    (None, Some(b)) => Some(b),
4029                    _ => None,
4030                }
4031            }
4032            NodeKind::Match { arms, .. } => {
4033                let mut common: Option<String> = None;
4034                for arm in arms {
4035                    let NodeKind::MatchArm { body, .. } = &arm.kind else {
4036                        continue;
4037                    };
4038                    let Some(ty) = self.infer_block_tail_type(body) else {
4039                        // A value-less arm (early-return) does not constrain.
4040                        continue;
4041                    };
4042                    match &common {
4043                        Some(c) if *c != ty => return None,
4044                        Some(_) => {}
4045                        None => common = Some(ty),
4046                    }
4047                }
4048                common
4049            }
4050            _ => None,
4051        }
4052    }
4053
4054    /// Infer the Go type of a block's value tail: for a `Block` the tail
4055    /// expression's type (a value-producing tail only — a statement tail like an
4056    /// early `return` yields `None`, as the block contributes no value), and for
4057    /// a bare expression body the expression's type. A nested `if`/`match` tail
4058    /// recurses through [`Self::infer_branchy_expr_type`]. `None` when no value
4059    /// type is determinable.
4060    fn infer_block_tail_type(&self, node: &AIRNode) -> Option<String> {
4061        match &node.kind {
4062            NodeKind::Block { tail, .. } => {
4063                let t = tail.as_deref()?;
4064                if crate::generator::node_is_statement(t) {
4065                    return None;
4066                }
4067                self.infer_block_tail_type(t)
4068            }
4069            NodeKind::If { .. } | NodeKind::Match { .. } => self.infer_branchy_expr_type(node),
4070            _ => self.infer_go_expr_type(node),
4071        }
4072    }
4073
4074    fn infer_go_expr_type(&self, node: &AIRNode) -> Option<String> {
4075        match &node.kind {
4076            NodeKind::Literal { lit } => match lit {
4077                Literal::Int(_) => Some("int64".to_string()),
4078                Literal::Float(_) => Some("float64".to_string()),
4079                Literal::Bool(_) => Some("bool".to_string()),
4080                Literal::String(_) => Some("string".to_string()),
4081                Literal::Char(_) => Some("rune".to_string()),
4082                Literal::Unit => None,
4083            },
4084            NodeKind::Identifier { name } => {
4085                // A bare reference to a *unit* user-enum variant (`Text`,
4086                // `HealthCheck`) types to its owning sealed-interface enum
4087                // (`MessageType`, `Route`), so an untyped `let t = Text` — or an
4088                // `if`/`match` whose arms yield such variants — infers the enum
4089                // type rather than collapsing to `interface{}`/the enclosing fn's
4090                // return type. Bound locals/params still win (a variable shadowing
4091                // is impossible — variant names are PascalCase, value idents are
4092                // camelCase — but check the var map first for symmetry).
4093                if let Some(t) = self.var_go_type.get(&go_value_ident(&name.name)) {
4094                    return Some(t.clone());
4095                }
4096                // Bare `None` lowers to the runtime `__bockOption` (the nullary
4097                // Optional constructor); type it so a value-CF arm yielding `None`
4098                // unifies with a sibling `Some(x)` arm (both `__bockOption`).
4099                if name.name == "None" {
4100                    return Some("__bockOption".to_string());
4101                }
4102                self.user_variant_for_name(&name.name)
4103                    .map(|info| info.enum_name.clone())
4104            }
4105            NodeKind::Interpolation { .. } => Some("string".to_string()),
4106            NodeKind::UnaryOp { op, operand } => match op {
4107                UnaryOp::Not => Some("bool".to_string()),
4108                UnaryOp::Neg | UnaryOp::BitNot => self.infer_go_expr_type(operand),
4109            },
4110            NodeKind::BinaryOp { op, left, right } => match op {
4111                BinOp::Eq
4112                | BinOp::Ne
4113                | BinOp::Lt
4114                | BinOp::Le
4115                | BinOp::Gt
4116                | BinOp::Ge
4117                | BinOp::And
4118                | BinOp::Or
4119                | BinOp::Is => Some("bool".to_string()),
4120                BinOp::Add
4121                | BinOp::Sub
4122                | BinOp::Mul
4123                | BinOp::Div
4124                | BinOp::Rem
4125                | BinOp::Pow
4126                | BinOp::BitAnd
4127                | BinOp::BitOr
4128                | BinOp::BitXor => {
4129                    // An arithmetic op is only soundly typed when neither operand
4130                    // is an *unresolved* identifier: a `func(x interface{}) ...`
4131                    // body of `x * 2` would not type-check in Go regardless of
4132                    // the literal, so leave the return type as `interface{}`
4133                    // rather than inferring a type the operation can't satisfy.
4134                    if Self::has_unresolved_operand(left, &self.var_go_type)
4135                        || Self::has_unresolved_operand(right, &self.var_go_type)
4136                    {
4137                        return None;
4138                    }
4139                    self.infer_go_expr_type(left)
4140                        .or_else(|| self.infer_go_expr_type(right))
4141                }
4142                BinOp::Compose => None,
4143            },
4144            // Collection literals so a nested collection (`[[1], [2]]`,
4145            // `{"k": [1, 2]}`) types its element concretely. A literal whose
4146            // elements infer to a single homogeneous Go type yields that
4147            // container type; otherwise `None` (callers fall back to
4148            // `interface{}`, never a wrong type).
4149            NodeKind::ListLiteral { elems } => self
4150                .infer_homogeneous_elem_type(elems)
4151                .map(|e| format!("[]{e}")),
4152            NodeKind::SetLiteral { elems } => self
4153                .infer_homogeneous_elem_type(elems)
4154                .map(|e| format!("map[{e}]struct{{}}")),
4155            NodeKind::MapLiteral { entries } => {
4156                let keys: Vec<&AIRNode> = entries.iter().map(|e| &e.key).collect();
4157                let vals: Vec<&AIRNode> = entries.iter().map(|e| &e.value).collect();
4158                match (
4159                    self.infer_homogeneous_elem_type_refs(&keys),
4160                    self.infer_homogeneous_elem_type_refs(&vals),
4161                ) {
4162                    (Some(k), Some(v)) => Some(format!("map[{k}]{v}")),
4163                    _ => None,
4164                }
4165            }
4166            // A record/struct-variant construction (`Item { id: 1 }`,
4167            // `Box[Item] { items: … }`) types to its Go struct name plus the
4168            // explicit type-arg suffix the emission would write (`Item`, or
4169            // `Box[int64]` when a param is recoverable from a directly-typed
4170            // field). This lets a list literal of record-constructs
4171            // (`[Item{…}, Item{…}]`) infer the homogeneous element type `Item` so
4172            // the `Box[T] { items: List[T] }` field literal emits `[]Item{…}`
4173            // rather than the erased `[]interface{}{…}` (GAP-A). Type args are
4174            // inferred from field values only (the `current_expected_type` used by
4175            // `expected_construct_type_args` names the *outer* binding, not the
4176            // per-element record); a generic param not directly typed by a field
4177            // falls back to `any`, matching the emission's loose-but-valid form.
4178            NodeKind::RecordConstruct { path, fields, .. } => {
4179                // A struct-payload variant construction (`GetUser { id: id }`)
4180                // types to its owning sealed-interface enum (`Route`), not the
4181                // variant struct (`RouteGetUser`): the value is boxed into the
4182                // interface at its use site, so an untyped binding / `if`-`else`
4183                // branch infers the assignable enum type.
4184                if let Some(info) = self.user_variant_for_path(path) {
4185                    return Some(info.enum_name.clone());
4186                }
4187                let type_name = self.record_construct_go_type_name(path);
4188                let type_args = self.infer_construct_type_args(&type_name, fields);
4189                Some(format!("{type_name}{type_args}"))
4190            }
4191            // A call to a known generic fn (`list_iter([]int64{...})`) resolves
4192            // to its return type with the type params bound from the arguments
4193            // (`ListIterator[int64]`), so a downstream call (`filter(it, ..)`)
4194            // can in turn bind its own params and specialise its lambda arg.
4195            NodeKind::Call { callee, args, .. } => {
4196                // A builtin String-method call types to its lowering's concrete
4197                // Go type (`p.trim()` → `string`, `s.split(..)` → `[]string`),
4198                // so a closure body / binding built from one infers concretely
4199                // (Q-go-split-combinator-typing). Checked before the user-method
4200                // return lookup: the receiver-kind annotation proves this is the
4201                // builtin, not a same-named user method.
4202                if let Some(t) = Self::string_builtin_return_go_type(node, callee, args) {
4203                    return Some(t);
4204                }
4205                let name = match &callee.kind {
4206                    NodeKind::Identifier { name } => name,
4207                    // A method call `recv.method(...)` lowers to a `Call` whose
4208                    // callee is a `FieldAccess`. Resolve it to the method's
4209                    // recorded Go return type (keyed by method name; the pre-scan
4210                    // omits names shared by methods with disagreeing returns, so a
4211                    // present entry is unambiguous). This types a
4212                    // `.map((p) => p.stock_value())` closure body to `float64`,
4213                    // sizing the result slice as `[]float64` rather than the
4214                    // erased `[]interface{}` whose elements a later `fold`'s
4215                    // `acc + v` cannot add.
4216                    NodeKind::FieldAccess { field, .. } => {
4217                        return self.method_return_go_types.get(&field.name).cloned();
4218                    }
4219                    _ => return None,
4220                };
4221                // The Optional/Result constructors lower to the runtime tagged
4222                // structs `__bockOption` / `__bockResult`, so a value-position
4223                // `if`/`match`/`loop` whose arms yield `Some(x)`/`None` (or `Ok`/
4224                // `Err`) infers that runtime type — letting the shared value-CF
4225                // hoist's `var __bock_cf_N __bockOption` be assignable to an
4226                // `Optional[T]`-returning fn (else it falls back to `interface{}`,
4227                // which Go rejects assigning into the typed return).
4228                match name.name.as_str() {
4229                    "Some" | "None" => return Some("__bockOption".to_string()),
4230                    "Ok" | "Err" => return Some("__bockResult".to_string()),
4231                    _ => {}
4232                }
4233                // A call to a local variable bound to a lambda (`clip_fn(x)`)
4234                // resolves to that lambda's recorded return type.
4235                if let Some(r) = self.var_lambda_ret.get(&go_value_ident(&name.name)) {
4236                    return Some(r.clone());
4237                }
4238                // A tuple-payload variant construction (`Circle(10)`) types to its
4239                // owning sealed-interface enum (`Shape`), mirroring the unit /
4240                // struct-payload variant cases — the variant struct is boxed into
4241                // the interface at its use site.
4242                if let Some(info) = self.user_variant_for_name(&name.name) {
4243                    return Some(info.enum_name.clone());
4244                }
4245                // A non-generic fn (`key`) resolves directly to its recorded Go
4246                // return type — no type-param binding needed. This is what types a
4247                // `[key(3), key(1)]` literal as `[]Key`.
4248                let Some((gp_names, param_tys, ret_ty)) = self.fn_signatures.get(&name.name) else {
4249                    return self.fn_return_go_types.get(&name.name).cloned();
4250                };
4251                let ret = ret_ty.as_ref()?;
4252                let bindings = self.bind_fn_type_params(gp_names, param_tys, args);
4253                let mut rendered = self.type_to_go(ret);
4254                for g in gp_names {
4255                    if let Some(b) = bindings.get(g) {
4256                        rendered = Self::replace_type_token(&rendered, g, b);
4257                    }
4258                }
4259                // Only return a fully-resolved type (no generic param left).
4260                if gp_names
4261                    .iter()
4262                    .any(|g| Self::contains_type_token(&rendered, g))
4263                {
4264                    return None;
4265                }
4266                Some(rendered)
4267            }
4268            // A bare `obj.field` access where `obj` is a variable of a known
4269            // record type resolves to the field's recorded Go type. This types a
4270            // `.map((b) => b.id)` closure body to `int64` (for `b: Block`,
4271            // `record Block { id: Int }`), sizing the `map` result slice as
4272            // `[]int64` rather than the erased `[]interface{}` Go rejects against
4273            // a declared `[]int64` return. `self.field` resolves through the
4274            // current-impl record; a non-identifier object (a chained access) is
4275            // left unresolved (conservative — `interface{}`, never wrong).
4276            NodeKind::FieldAccess { object, field } => {
4277                let record = match &object.kind {
4278                    NodeKind::Identifier { name } if name.name == "self" => {
4279                        self.current_self_record.clone()?
4280                    }
4281                    NodeKind::Identifier { name } => {
4282                        let obj_go_ty = self.var_go_type.get(&go_value_ident(&name.name))?;
4283                        Self::go_type_record_head(obj_go_ty).to_string()
4284                    }
4285                    _ => return None,
4286                };
4287                self.record_field_go_type
4288                    .get(&record)
4289                    .and_then(|m| m.get(&field.name))
4290                    .cloned()
4291            }
4292            // A `recv.method()` call resolves to the method's recorded Go return
4293            // type. Keyed by method name only; the pre-scan poisons (omits) any
4294            // name shared by methods with disagreeing return types, so a present
4295            // entry is unambiguous. Lets a `.map((p) => p.stock_value())` closure
4296            // body type to `float64`, sizing the result slice as `[]float64`.
4297            // A `MethodCall` node (the non-desugared method-call form) resolves
4298            // the same way as the `Call`-with-`FieldAccess` form above.
4299            NodeKind::MethodCall { method, .. } => {
4300                self.method_return_go_types.get(&method.name).cloned()
4301            }
4302            _ => None,
4303        }
4304    }
4305
4306    /// Infer a single homogeneous Go element type for a collection literal's
4307    /// elements: `Some(ty)` iff the literal is non-empty and EVERY element
4308    /// infers (via [`Self::infer_go_expr_type`]) to the *same* concrete Go type.
4309    /// An empty literal, an element whose type can't be inferred, or a mix of
4310    /// types yields `None` — the caller then emits `interface{}`, which is never
4311    /// wrong (only loose). The `has_unresolved_operand` guard inside
4312    /// `infer_go_expr_type` already keeps arithmetic over unresolved identifiers
4313    /// from inferring an unsound type.
4314    fn infer_homogeneous_elem_type(&self, elems: &[AIRNode]) -> Option<String> {
4315        let refs: Vec<&AIRNode> = elems.iter().collect();
4316        self.infer_homogeneous_elem_type_refs(&refs)
4317    }
4318
4319    /// `&AIRNode`-slice variant of [`Self::infer_homogeneous_elem_type`] (used
4320    /// for `MapLiteral` keys/values, which are not stored as a contiguous
4321    /// `&[AIRNode]`).
4322    fn infer_homogeneous_elem_type_refs(&self, elems: &[&AIRNode]) -> Option<String> {
4323        let mut iter = elems.iter();
4324        let first = self.infer_go_expr_type(iter.next()?)?;
4325        for e in iter {
4326            if self.infer_go_expr_type(e)? != first {
4327                return None;
4328            }
4329        }
4330        Some(first)
4331    }
4332
4333    /// Build the explicit type-argument suffix (`[int64]`, `[int64, string]`)
4334    /// for a generic struct construction. For each of the target record's
4335    /// generic params (in declaration order) it finds the field whose declared
4336    /// type is exactly that param, then infers that field value's Go type. A
4337    /// param with no directly-typed field, or a value whose type can't be
4338    /// inferred, falls back to `any` (still a valid, if loose, instantiation).
4339    /// Returns `""` for a non-generic / unregistered type.
4340    /// The explicit Go type-argument suffix (`[int64]`) for a generic struct
4341    /// construction, recovered from the *declared* binding/expected type when it
4342    /// names this exact record (`current_expected_type == "ListIter[int64]"` for
4343    /// a `ListIter { ... }` construction). Returns `Some("[int64]")` then,
4344    /// `None` when there is no expected type, it names a different type, or it
4345    /// carries no args. More robust than field-value inference: it works when a
4346    /// generic param appears only *nested* in a field type (`xs: List[T]`),
4347    /// where no field is typed exactly `T`.
4348    fn expected_construct_type_args(&self, type_name: &str) -> Option<String> {
4349        let expected = self.current_expected_type.as_deref()?;
4350        let rest = expected.strip_prefix(type_name)?;
4351        // The remainder must be exactly a `[...]` type-arg list (so `ListIter`
4352        // does not match a hypothetical `ListIterator`); reject an empty suffix
4353        // (`ListIter` with no args) and anything not enclosed in brackets.
4354        if rest.starts_with('[') && rest.ends_with(']') && rest.len() > 2 {
4355            Some(rest.to_string())
4356        } else {
4357            None
4358        }
4359    }
4360
4361    /// The concrete Go type-argument suffix (`[int64]`) for a *generic user enum*
4362    /// instantiation, recovered from a fully-rendered enum type string such as
4363    /// `Box[int64]`. Go has no sum type, so a generic user enum lowers to a
4364    /// sealed interface plus per-variant structs that ALL carry the enum's
4365    /// type-param list (`type BoxFull[T any] struct{…}`); every *use* of a
4366    /// variant struct — a construction (`BoxFull[int64]{…}`) and a type-switch
4367    /// `case` (`case BoxFull[int64]:`) — must therefore spell the concrete args,
4368    /// because Go rejects a bare generic type without instantiation. The args are
4369    /// the same for every variant of one enum (they share the enum's params), so
4370    /// they are read off the enum instantiation rather than per-variant.
4371    ///
4372    /// Returns `""` (no suffix) when the enum is non-generic, unregistered, or
4373    /// `full_type` is not exactly `<enum_name>[...]` — keeping a non-generic enum
4374    /// emitting the bare `BoxEmpty{}` / `case ShapeCircle:` it always did.
4375    fn enum_variant_type_arg_suffix(&self, enum_name: &str, full_type: &str) -> String {
4376        // A non-generic enum carries no params: never append a suffix.
4377        if self
4378            .generic_decls
4379            .get(enum_name)
4380            .is_none_or(|p| p.is_empty())
4381        {
4382            return String::new();
4383        }
4384        let Some(rest) = full_type.strip_prefix(enum_name) else {
4385            return String::new();
4386        };
4387        // The remainder must be exactly a `[...]` arg list (so `Box` does not
4388        // match `Boxer[…]`); reject an empty / unbracketed suffix.
4389        if rest.starts_with('[') && rest.ends_with(']') && rest.len() > 2 {
4390            rest.to_string()
4391        } else {
4392            String::new()
4393        }
4394    }
4395
4396    /// The concrete type-arg suffix (`[int64]`) for constructing a variant of the
4397    /// generic user enum `enum_name`, read from the binding's *expected* type
4398    /// (`current_expected_type`, e.g. `Box[int64]` for `let f: Box[Int] =
4399    /// Full(7)`). Returns `""` when there is no expected type, it names a
4400    /// different enum, or the enum is non-generic. The construction-site analogue
4401    /// of [`Self::enum_variant_type_arg_suffix`].
4402    fn expected_enum_variant_type_arg_suffix(&self, enum_name: &str) -> String {
4403        match self.current_expected_type.as_deref() {
4404            Some(expected) => self.enum_variant_type_arg_suffix(enum_name, expected),
4405            None => String::new(),
4406        }
4407    }
4408
4409    fn infer_construct_type_args(
4410        &self,
4411        type_name: &str,
4412        fields: &[bock_air::AirRecordField],
4413    ) -> String {
4414        let Some(per_param) = self.record_param_fields.get(type_name) else {
4415            return String::new();
4416        };
4417        if per_param.is_empty() {
4418            return String::new();
4419        }
4420        let args: Vec<String> = per_param
4421            .iter()
4422            .map(|field_name| {
4423                field_name
4424                    .as_ref()
4425                    .and_then(|fname| {
4426                        fields
4427                            .iter()
4428                            .find(|f| &f.name.name == fname)
4429                            .and_then(|f| f.value.as_deref())
4430                            .and_then(|v| self.infer_go_expr_type(v))
4431                    })
4432                    .unwrap_or_else(|| "any".to_string())
4433            })
4434            .collect();
4435        format!("[{}]", args.join(", "))
4436    }
4437
4438    /// Record the `Optional[T]`, `List[T]`, `Map[K, V]`, `Set[E]`, and
4439    /// `Result[T, E]` element Go types of a function/lambda's parameters into the
4440    /// variable scopes, so a `match param { Some(x) => ... }` (direct Optional),
4441    /// `match param.get(i) { Some(x) => ... }` (List/Map built-in), or a `Set`
4442    /// membership test inside the body type-checks against the concrete element
4443    /// type. Returns the previous `(var_optional_elem, var_list_elem,
4444    /// var_result_elem, var_map_kv, var_set_elem)` scopes so the caller can
4445    /// restore them on exit (Go has no block-scoped reset here).
4446    #[allow(clippy::type_complexity)]
4447    fn enter_param_optional_scope(
4448        &mut self,
4449        params: &[AIRNode],
4450    ) -> (
4451        HashMap<String, String>,
4452        HashMap<String, String>,
4453        HashMap<String, (String, String)>,
4454        HashMap<String, (String, String)>,
4455        HashMap<String, String>,
4456    ) {
4457        let saved_opt = self.var_optional_elem.clone();
4458        let saved_list = self.var_list_elem.clone();
4459        let saved_result = self.var_result_elem.clone();
4460        let saved_map = self.var_map_kv.clone();
4461        let saved_set = self.var_set_elem.clone();
4462        for p in params {
4463            if let NodeKind::Param {
4464                pattern,
4465                ty: Some(t),
4466                ..
4467            } = &p.kind
4468            {
4469                let name = self.pattern_to_binding_name(pattern);
4470                // Record the full declared type node so a `match` whose
4471                // scrutinee is this param can peel a *nested* Optional/Result to
4472                // assert a tuple payload to its concrete struct (see
4473                // `var_decl_type_node`).
4474                self.var_decl_type_node.insert(name.clone(), (**t).clone());
4475                if let Some(elem) = self.optional_elem_go_type(t) {
4476                    self.var_optional_elem.insert(name.clone(), elem);
4477                }
4478                if let Some(elem) = self.list_elem_go_type(t) {
4479                    self.var_list_elem.insert(name.clone(), elem);
4480                }
4481                if let Some(kv) = self.map_kv_go_types(t) {
4482                    self.var_map_kv.insert(name.clone(), kv);
4483                }
4484                if let Some(elem) = self.set_elem_go_type(t) {
4485                    self.var_set_elem.insert(name.clone(), elem);
4486                }
4487                if let Some(elems) = self.result_elem_go_types(t) {
4488                    self.var_result_elem.insert(name.clone(), elems);
4489                }
4490                // A generic-record-typed param (`c: Counter[Int]`) records its
4491                // concrete instantiation so a `match c.next() { Some(x) => ... }`
4492                // can resolve the generic `Optional[T]` payload to the concrete
4493                // arg (`int64`) — see `scrutinee_optional_elem`.
4494                if let Some(record_args) = self.record_type_args(t) {
4495                    self.var_record_type_args.insert(name, record_args);
4496                }
4497            }
4498        }
4499        (saved_opt, saved_list, saved_result, saved_map, saved_set)
4500    }
4501
4502    /// Record each typed param's Go type into [`Self::var_go_type`] so the
4503    /// body's expression types can be inferred (chiefly to give a lambda a
4504    /// concrete return type). Returns the previous map so the caller can restore
4505    /// it on exit. Untyped params are skipped (left absent → inference yields
4506    /// the `interface{}` fallback, never a wrong type).
4507    /// Record each param's Go type into the variable scope so the body's
4508    /// `infer_go_expr_type` sees concrete param types. A param whose source type
4509    /// is absent (an untyped lambda param) takes its type from the positional
4510    /// `expected` entry (the callee-specialised type, e.g. `int64`) when
4511    /// present, so `x > 2` / `x * 2` type-check and the lambda's inferred return
4512    /// type is concrete. Returns the previous scope for restore on exit. Pass
4513    /// `None` for `expected` when there are no specialised types (an ordinary
4514    /// typed lambda / fn body).
4515    /// The emitted Go binding names of a function/method's value parameters,
4516    /// used to pre-seed the body's Go block frame for shadowing-`let` tracking.
4517    fn param_binding_names(&self, params: &[AIRNode]) -> Vec<String> {
4518        params
4519            .iter()
4520            .filter_map(|p| match &p.kind {
4521                NodeKind::Param { pattern, .. } => {
4522                    let n = self.pattern_to_binding_name(pattern);
4523                    (n != "_").then_some(n)
4524                }
4525                _ => None,
4526            })
4527            .collect()
4528    }
4529
4530    fn enter_param_go_types_with_expected(
4531        &mut self,
4532        params: &[AIRNode],
4533        expected: Option<&[String]>,
4534    ) -> HashMap<String, String> {
4535        let saved = self.var_go_type.clone();
4536        for (i, p) in params.iter().enumerate() {
4537            if let NodeKind::Param { pattern, ty, .. } = &p.kind {
4538                let name = self.pattern_to_binding_name(pattern);
4539                let go_ty = ty
4540                    .as_deref()
4541                    .map(|t| self.type_to_go(t))
4542                    .or_else(|| expected.and_then(|e| e.get(i).cloned()));
4543                if let Some(g) = go_ty {
4544                    self.var_go_type.insert(name, g);
4545                }
4546            }
4547        }
4548        saved
4549    }
4550
4551    /// Render lambda params with explicit Go types drawn from `types` (one per
4552    /// param, positionally) — used when a lambda argument is specialised to a
4553    /// callee's concrete parameter types. A param with its own source
4554    /// annotation keeps it; otherwise the positional `types` entry is used.
4555    fn collect_param_strs_with_types(&self, params: &[AIRNode], types: &[String]) -> Vec<String> {
4556        params
4557            .iter()
4558            .enumerate()
4559            .filter_map(|(i, p)| {
4560                if let NodeKind::Param { pattern, ty, .. } = &p.kind {
4561                    let name = self.pattern_to_binding_name(pattern);
4562                    let type_str = ty
4563                        .as_ref()
4564                        .map(|t| self.type_to_go(t))
4565                        .or_else(|| types.get(i).cloned())
4566                        .unwrap_or_else(|| "interface{}".into());
4567                    Some(format!("{name} {type_str}"))
4568                } else {
4569                    None
4570                }
4571            })
4572            .collect()
4573    }
4574
4575    /// Resolve the Go element type to assert for the payload of a `Some` bound in
4576    /// a `match` on `scrutinee`. Reachable for the common, structurally
4577    /// determinable cases: an identifier (parameter or typed `let`), a call to a
4578    /// function with a known `Optional[T]` return, and a *method call* whose
4579    /// method has a known `Optional[T]` return (`match it.next() { Some(x) =>
4580    /// ... }`, the shape `for x in <Iterable>` desugars to). Returns `None` when
4581    /// the element type cannot be determined structurally, in which case the
4582    /// binding is left as the runtime `interface{}` (no regression: that is the
4583    /// prior behavior, and `${v}`-style interpolation still works).
4584    /// Resolve a method-call's `Optional[T]` payload element to its CONCRETE Go
4585    /// type at the call site. `method_optional_ret_elem` stores the *generic*
4586    /// element as written on the method (`"T"`, the record's type param), which
4587    /// is undefined in a concrete caller such as `main`. When `receiver` is a
4588    /// variable bound to a concrete generic-record instantiation (recorded in
4589    /// [`Self::var_record_type_args`], e.g. `c: ListIter[Int]` →
4590    /// `("ListIter", ["int64"])`), and `elem` names one of that record's generic
4591    /// params, substitute the param with the corresponding concrete arg
4592    /// (`"T"` → `"int64"`). Otherwise `elem` is already concrete (a non-generic
4593    /// method, or a param-less return) and is returned unchanged.
4594    fn resolve_concrete_method_elem(&self, receiver: &AIRNode, elem: &str) -> String {
4595        let NodeKind::Identifier { name } = &receiver.kind else {
4596            return elem.to_string();
4597        };
4598        let Some((base, args)) = self.var_record_type_args.get(&go_value_ident(&name.name)) else {
4599            return elem.to_string();
4600        };
4601        let Some(params) = self.generic_decls.get(base) else {
4602            return elem.to_string();
4603        };
4604        // Find the generic param whose name equals `elem`, then map to the arg.
4605        if let Some(idx) = params.iter().position(|p| p.name.name == elem) {
4606            if let Some(concrete) = args.get(idx) {
4607                return concrete.clone();
4608            }
4609        }
4610        elem.to_string()
4611    }
4612
4613    fn scrutinee_optional_elem(&self, scrutinee: &AIRNode) -> Option<String> {
4614        match &scrutinee.kind {
4615            NodeKind::Identifier { name } => self
4616                .var_optional_elem
4617                .get(&go_value_ident(&name.name))
4618                .cloned(),
4619            // A direct method call (`it.next()`).
4620            NodeKind::MethodCall {
4621                receiver, method, ..
4622            } => {
4623                let elem = self.method_optional_ret_elem.get(&method.name).cloned()?;
4624                Some(self.resolve_concrete_method_elem(receiver, &elem))
4625            }
4626            NodeKind::Call { callee, args, .. } => {
4627                // The read-only `List` built-ins `get`/`first`/`last` return
4628                // `Optional[<elem>]`. When the receiver is a variable with a
4629                // known `List[T]` element type, that element type is the payload
4630                // type — resolve it from `var_list_elem` before the generic
4631                // method-call path (whose `method_optional_ret_elem` only knows
4632                // *user-defined* methods, never the List built-ins).
4633                if let Some((recv, method, _)) =
4634                    crate::generator::desugared_list_method(scrutinee, callee, args)
4635                {
4636                    if matches!(method, "get" | "first" | "last") {
4637                        // The same receiver-element resolver the `.get` closure
4638                        // uses: a `List[T]` identifier (via `var_list_elem`), a
4639                        // homogeneous list literal, `self.field`, or a generic
4640                        // record param's `value.field` (`b.items.get(i)` for
4641                        // `b: Box[T]`). Without the last case the `Some(x)` payload
4642                        // stayed `interface{}` and a `return x` of a `[]T`-typed
4643                        // field element failed `go build` (GAP-A).
4644                        if let Some(elem) = self.list_receiver_elem_go_type(recv) {
4645                            return Some(elem);
4646                        }
4647                    }
4648                }
4649                // DQ30: `pop` on a `List[T]` returns `Optional[T]` — resolve
4650                // the payload the same way `get`/`first`/`last` do, so
4651                // `match xs.pop() { Some(v) => … }` type-asserts `v` to the
4652                // element type rather than `interface{}`.
4653                if let Some((recv, "pop", _)) =
4654                    crate::generator::desugared_list_inplace_mutator(scrutinee, callee, args)
4655                {
4656                    if let Some(elem) = self.list_receiver_elem_go_type(recv) {
4657                        return Some(elem);
4658                    }
4659                }
4660                // `Map.get(k)` returns `Optional[V]`; resolve the payload to the
4661                // map's value Go type so `match m.get(k) { Some(x) => … }`
4662                // type-asserts `x` to `V` rather than `interface{}`.
4663                if let Some((recv, "get", _)) =
4664                    crate::generator::desugared_map_method(scrutinee, callee, args)
4665                {
4666                    if let Some((_k, v)) = self.map_receiver_kv_go_types(recv) {
4667                        return Some(v);
4668                    }
4669                }
4670                match &callee.kind {
4671                    // Free-function call (`firstPositive(a, b)`).
4672                    NodeKind::Identifier { name } => {
4673                        self.fn_optional_ret_elem.get(&name.name).cloned()
4674                    }
4675                    // The AIR also lowers `recv.method(rest)` into
4676                    // `Call(FieldAccess(recv, method), [recv, ...rest])`; resolve
4677                    // it the same way as a direct `MethodCall` so both desugar
4678                    // shapes get a type-asserted payload.
4679                    NodeKind::FieldAccess { object, field } => {
4680                        crate::generator::desugared_self_call(callee, args)?;
4681                        let elem = self.method_optional_ret_elem.get(&field.name).cloned()?;
4682                        Some(self.resolve_concrete_method_elem(object, &elem))
4683                    }
4684                    _ => None,
4685                }
4686            }
4687            _ => None,
4688        }
4689    }
4690
4691    /// Resolve the `(ok_go_type, err_go_type)` to assert for the payload of an
4692    /// `Ok`/`Err` bound in a `match` on `scrutinee`. The Result analogue of
4693    /// [`Self::scrutinee_optional_elem`]: an identifier (parameter or typed
4694    /// `let`) or a call to a function with a known `Result[T, E]` return.
4695    /// Returns `None` when the types cannot be determined structurally, in which
4696    /// case the payload falls back to the runtime `interface{}` (never wrong,
4697    /// only un-asserted).
4698    fn scrutinee_result_elems(&self, scrutinee: &AIRNode) -> Option<(String, String)> {
4699        match &scrutinee.kind {
4700            NodeKind::Identifier { name } => self
4701                .var_result_elem
4702                .get(&go_value_ident(&name.name))
4703                .cloned(),
4704            NodeKind::Call { callee, args, .. } => match &callee.kind {
4705                NodeKind::Identifier { name } => self.fn_result_ret_elem.get(&name.name).cloned(),
4706                NodeKind::FieldAccess { .. }
4707                    if crate::generator::desugared_self_call(callee, args).is_some() =>
4708                {
4709                    None
4710                }
4711                _ => None,
4712            },
4713            _ => None,
4714        }
4715    }
4716
4717    /// The declared type-expression AIR node of a match scrutinee, when it is a
4718    /// variable (parameter or typed `let`) recorded in [`Self::var_decl_type_node`].
4719    /// Returns `None` for any other scrutinee shape (a call, a method call, …) —
4720    /// the pattern recursion then leaves a nested tuple payload un-asserted (the
4721    /// prior `interface{}` behavior; never wrong, only un-typed). Used to seed
4722    /// the declared-type threading through the if-chain pattern lowering so a
4723    /// `match v { Some(Ok((a, b))) => … }` asserts its tuple payload.
4724    fn scrutinee_decl_type_node(&self, scrutinee: &AIRNode) -> Option<&AIRNode> {
4725        if let NodeKind::Identifier { name } = &scrutinee.kind {
4726            return self.var_decl_type_node.get(&go_value_ident(&name.name));
4727        }
4728        None
4729    }
4730
4731    /// The Go payload type of an *argument expression* whose static type is
4732    /// `Optional[T]`, when structurally recoverable. Extends
4733    /// [`Self::scrutinee_optional_elem`] (identifiers via `var_optional_elem`,
4734    /// calls via the fn/method return-element maps) with the bare-constructor
4735    /// case `Some(<expr>)` / `None`, whose payload type is inferred from the
4736    /// payload expression. Used to pin a generic free-fn's `Optional[T]`
4737    /// type-parameter at the call site (Go cannot infer it: the runtime
4738    /// `__bockOption` struct carries no `[T]`).
4739    fn arg_optional_elem(&self, arg: &AIRNode) -> Option<String> {
4740        if let NodeKind::Call { callee, args, .. } = &arg.kind {
4741            if let NodeKind::Identifier { name } = &callee.kind {
4742                match name.name.as_str() {
4743                    "Some" => return args.first().and_then(|a| self.infer_go_expr_type(&a.value)),
4744                    // `None` carries no payload type; nothing to bind.
4745                    "None" => return None,
4746                    _ => {}
4747                }
4748            }
4749        }
4750        self.scrutinee_optional_elem(arg)
4751    }
4752
4753    /// The `(ok, err)` Go payload types of an *argument expression* whose static
4754    /// type is `Result[T, E]`, when recoverable. The `Result` analogue of
4755    /// [`Self::arg_optional_elem`]: identifiers / calls via
4756    /// [`Self::scrutinee_result_elems`], plus the bare `Ok(<expr>)` / `Err(<expr>)`
4757    /// constructors (only the present arm's type is inferable from a bare
4758    /// constructor, so the other stays `None`).
4759    fn arg_result_elems(&self, arg: &AIRNode) -> (Option<String>, Option<String>) {
4760        if let NodeKind::Call { callee, args, .. } = &arg.kind {
4761            if let NodeKind::Identifier { name } = &callee.kind {
4762                match name.name.as_str() {
4763                    "Ok" => {
4764                        return (
4765                            args.first().and_then(|a| self.infer_go_expr_type(&a.value)),
4766                            None,
4767                        )
4768                    }
4769                    "Err" => {
4770                        return (
4771                            None,
4772                            args.first().and_then(|a| self.infer_go_expr_type(&a.value)),
4773                        )
4774                    }
4775                    _ => {}
4776                }
4777            }
4778        }
4779        match self.scrutinee_result_elems(arg) {
4780            Some((ok, err)) => (Some(ok), Some(err)),
4781            None => (None, None),
4782        }
4783    }
4784
4785    /// The `Optional[T]` inner / `Result[T, E]` arg type-param names of a
4786    /// declared parameter (or return) AIR type, when the type is one of those
4787    /// containers. Returns the param-name tokens (`["T"]` for `Optional[T]`,
4788    /// `["T", "E"]` for `Result[T, E]`) so the caller can pair them with the
4789    /// argument's recovered element types. `None` for any other type.
4790    fn container_type_param_names(node: &AIRNode) -> Option<(&'static str, Vec<&str>)> {
4791        match &node.kind {
4792            NodeKind::TypeOptional { inner } => {
4793                Some(("Optional", vec![Self::type_param_token(inner)?]))
4794            }
4795            NodeKind::TypeNamed { path, args } => {
4796                let name = path.segments.last().map(|s| s.name.as_str())?;
4797                match name {
4798                    "Optional" => Some(("Optional", vec![Self::type_param_token(args.first()?)?])),
4799                    "Result" => {
4800                        let t = Self::type_param_token(args.first()?)?;
4801                        let e = Self::type_param_token(args.get(1)?)?;
4802                        Some(("Result", vec![t, e]))
4803                    }
4804                    _ => None,
4805                }
4806            }
4807            _ => None,
4808        }
4809    }
4810
4811    /// The bare type-parameter name a type node names, if it is a single
4812    /// unparameterised `TypeNamed` segment (`T` → `Some("T")`). `None` for any
4813    /// composite or primitive type — only a bare name can be bound positionally
4814    /// from a container's recovered element type.
4815    fn type_param_token(node: &AIRNode) -> Option<&str> {
4816        if let NodeKind::TypeNamed { path, args } = &node.kind {
4817            if args.is_empty() && path.segments.len() == 1 {
4818                return path.segments.first().map(|s| s.name.as_str());
4819            }
4820        }
4821        None
4822    }
4823
4824    /// Synthesise the explicit Go type-arguments (`[int64]`, `[int64, string]`)
4825    /// for a call to a generic free function whose source omits them, so a Go
4826    /// type-parameter that the runtime cannot infer is pinned without a
4827    /// turbofish at the Bock level.
4828    ///
4829    /// Go can only infer a type-parameter from a value argument's static type.
4830    /// A parameter declared `Optional[T]` / `Result[T, E]` lowers to the
4831    /// *monomorphic* runtime struct `__bockOption` / `__bockResult` (payload
4832    /// `interface{}`), so its `T`/`E` are invisible to Go inference and a call
4833    /// like `or_else(empty, Some(7))` fails `cannot infer T`. This recovers each
4834    /// type-parameter's concrete Go type from, in order:
4835    ///
4836    /// 1. an `Optional[T]` / `Result[T, E]` argument's recovered element type
4837    ///    ([`Self::arg_optional_elem`] / [`Self::arg_result_elems`]),
4838    /// 2. an ordinary (non-container, non-lambda) argument unified against its
4839    ///    declared param type ([`Self::bind_fn_type_params`] — pins e.g.
4840    ///    `get_or`'s `fallback: T`),
4841    /// 3. the call's *expected* result type ([`Self::current_expected_type`],
4842    ///    a typed `let x: Ty = …`) unified against the declared return type.
4843    ///
4844    /// Returns `Some([go-type, …])` in declaration order, with any
4845    /// type-parameter still unresolved after the three sources filled with `any`
4846    /// (such a parameter appears *only* behind the erased runtime, so `any` is
4847    /// its only consistent type — e.g. `and_then`'s mapped `U`). Returns `None`
4848    /// — emitting no turbofish, so ordinary Go inference runs — when the
4849    /// signature names no `Optional`/`Result` container at all (the `core.iter`
4850    /// generic-record combinators, which Go already infers and must not change).
4851    fn synthesize_go_type_args(
4852        &self,
4853        gp_names: &[String],
4854        param_tys: &[Option<AIRNode>],
4855        ret_ty: Option<&AIRNode>,
4856        args: &[bock_air::AirArg],
4857        force: bool,
4858    ) -> Option<Vec<String>> {
4859        if gp_names.is_empty() {
4860            return None;
4861        }
4862        // Only intervene for a fn whose signature involves the monomorphic
4863        // `Optional`/`Result` runtime — the case Go's own inference cannot
4864        // handle (`__bockOption`/`__bockResult` carry no `[T]`) — or one whose
4865        // sealed-core bound was lowered to a built-in constraint (`force`), under
4866        // which Go infers an untyped constant as the default type (`int`, not
4867        // `int64`). A purely record/collection-generic fn (`core.iter`'s
4868        // `ListIterator[T]` combinators) is left bare so Go infers it as before —
4869        // no regression.
4870        let touches_container = param_tys
4871            .iter()
4872            .flatten()
4873            .chain(ret_ty)
4874            .any(Self::type_mentions_container);
4875        // A type param that appears in *no* value parameter cannot be inferred by
4876        // Go from the arguments — it is pinned only by the return type (e.g.
4877        // `fn empty[T]() -> SortedSet[T]`, a zero-arg generic constructor, or any
4878        // return-only param). Such a call always needs an explicit turbofish,
4879        // synthesised below from the expected destination type. (A param that
4880        // *does* appear in a value parameter is left to Go's own inference unless
4881        // a container/sealed-bound reason forces the turbofish.)
4882        let param_go_types: Vec<String> = param_tys
4883            .iter()
4884            .flatten()
4885            .map(|p| self.type_to_go(p))
4886            .collect();
4887        let has_return_only_param = gp_names.iter().any(|g| {
4888            !param_go_types
4889                .iter()
4890                .any(|p| Self::contains_type_token(p, g))
4891        });
4892        if !touches_container && !force && !has_return_only_param {
4893            return None;
4894        }
4895        let mut bindings: HashMap<String, String> = HashMap::new();
4896
4897        // (2) Ordinary argument unification (bare `T`, `List[T]`, etc.).
4898        for (k, v) in self.bind_fn_type_params(gp_names, param_tys, args) {
4899            bindings.entry(k).or_insert(v);
4900        }
4901
4902        // (1) Container arguments: pair each declared `Optional[T]`/`Result[T,E]`
4903        // param's type-param tokens with the argument's recovered element types.
4904        for (i, arg) in args.iter().enumerate() {
4905            let Some(pty) = param_tys.get(i).and_then(|p| p.as_ref()) else {
4906                continue;
4907            };
4908            let Some((container, tokens)) = Self::container_type_param_names(pty) else {
4909                continue;
4910            };
4911            match container {
4912                "Optional" => {
4913                    if let (Some(token), Some(elem)) =
4914                        (tokens.first(), self.arg_optional_elem(&arg.value))
4915                    {
4916                        if gp_names.iter().any(|g| g == *token) {
4917                            bindings.entry((*token).to_string()).or_insert(elem);
4918                        }
4919                    }
4920                }
4921                "Result" => {
4922                    let (ok, err) = self.arg_result_elems(&arg.value);
4923                    if let (Some(token), Some(ty)) = (tokens.first(), ok) {
4924                        if gp_names.iter().any(|g| g == *token) {
4925                            bindings.entry((*token).to_string()).or_insert(ty);
4926                        }
4927                    }
4928                    if let (Some(token), Some(ty)) = (tokens.get(1), err) {
4929                        if gp_names.iter().any(|g| g == *token) {
4930                            bindings.entry((*token).to_string()).or_insert(ty);
4931                        }
4932                    }
4933                }
4934                _ => {}
4935            }
4936        }
4937
4938        // (3) Expected result type unified against the declared return type. The
4939        // typed-`let` binding sets `current_expected_type` to the rendered Go
4940        // type of the destination, e.g. `[]int64` for `let xs: List[Int] =
4941        // to_list(...)`, which unifies against `List[T]` → `[]T` to pin `T`.
4942        if let (Some(ret), Some(expected)) = (ret_ty, self.current_expected_type.as_deref()) {
4943            if expected != "interface{}" {
4944                let pattern = self.type_to_go(ret);
4945                Self::unify_go_pattern(&pattern, expected, gp_names, &mut bindings);
4946            }
4947        }
4948
4949        // Every type-parameter is filled: a pinned one with its concrete Go
4950        // type, an unresolved one with `any`. An unresolved param appears *only*
4951        // behind the erased `Optional`/`Result` runtime (a bare `T`, `List[T]`,
4952        // `Fn(T) -> …`, etc. would have been bound above from an argument or the
4953        // return), so `any` is the only type it can take and never conflicts —
4954        // e.g. `and_then`'s `U` (the mapped `Ok` type) is invisible to the call
4955        // and harmlessly erased, while its `E` is pinned from the `Result[T, E]`
4956        // argument so Go no longer fails `cannot infer E`.
4957        let out: Vec<String> = gp_names
4958            .iter()
4959            .map(|g| {
4960                bindings
4961                    .get(g)
4962                    .cloned()
4963                    .unwrap_or_else(|| "any".to_string())
4964            })
4965            .collect();
4966        Some(out)
4967    }
4968
4969    /// True if a declared AIR type *mentions* the monomorphic `Optional` /
4970    /// `Result` runtime anywhere within it (directly, or nested inside a
4971    /// collection / function type). Gates [`Self::synthesize_go_type_args`] —
4972    /// only such a signature defeats Go's own type-parameter inference.
4973    fn type_mentions_container(node: &AIRNode) -> bool {
4974        match &node.kind {
4975            NodeKind::TypeOptional { .. } => true,
4976            NodeKind::TypeNamed { path, args } => {
4977                let is_container = path
4978                    .segments
4979                    .last()
4980                    .is_some_and(|s| matches!(s.name.as_str(), "Optional" | "Result"));
4981                is_container || args.iter().any(Self::type_mentions_container)
4982            }
4983            NodeKind::TypeFunction { params, ret, .. } => {
4984                params.iter().any(Self::type_mentions_container)
4985                    || Self::type_mentions_container(ret)
4986            }
4987            NodeKind::TypeTuple { elems } => elems.iter().any(Self::type_mentions_container),
4988            _ => false,
4989        }
4990    }
4991
4992    /// Returns `true` if the AIR type node mentions any of the named generic
4993    /// params (a bare `T`, or `T` nested inside `List[T]` / `Optional[T]` /
4994    /// `(T, U)` / a function type). Used to skip recording a method's Go return
4995    /// type when it is still generic (the concrete caller has no such `T`).
4996    fn type_mentions_params(node: &AIRNode, params: &[String]) -> bool {
4997        match &node.kind {
4998            NodeKind::TypeNamed { path, args } => {
4999                let names_param = path
5000                    .segments
5001                    .last()
5002                    .is_some_and(|s| params.iter().any(|p| p == &s.name));
5003                names_param || args.iter().any(|a| Self::type_mentions_params(a, params))
5004            }
5005            NodeKind::TypeOptional { inner } => Self::type_mentions_params(inner, params),
5006            NodeKind::TypeFunction {
5007                params: ps, ret, ..
5008            } => {
5009                ps.iter().any(|p| Self::type_mentions_params(p, params))
5010                    || Self::type_mentions_params(ret, params)
5011            }
5012            NodeKind::TypeTuple { elems } => {
5013                elems.iter().any(|e| Self::type_mentions_params(e, params))
5014            }
5015            _ => false,
5016        }
5017    }
5018
5019    /// Returns `true` if the AIR type node represents `Void` or `Unit`.
5020    fn is_void_type(node: &AIRNode) -> bool {
5021        if let NodeKind::TypeNamed { path, .. } = &node.kind {
5022            if let Some(last) = path.segments.last() {
5023                return last.name == "Void" || last.name == "Unit";
5024            }
5025        }
5026        if let NodeKind::TypeTuple { elems } = &node.kind {
5027            return elems.is_empty();
5028        }
5029        false
5030    }
5031
5032    /// Returns `true` if the AST `TypeExpr` represents `Void` or `Unit` (the
5033    /// `TypeExpr` analogue of [`Self::is_void_type`]). Used by `ast_type_to_go`
5034    /// to render a `Fn(...) -> Void` as a Go `func(...)` with no result type.
5035    fn ast_type_is_void(ty: &TypeExpr) -> bool {
5036        match ty {
5037            TypeExpr::Named { path, args, .. } if args.is_empty() => path
5038                .segments
5039                .last()
5040                .is_some_and(|s| s.name == "Void" || s.name == "Unit"),
5041            TypeExpr::Tuple { elems, .. } => elems.is_empty(),
5042            _ => false,
5043        }
5044    }
5045
5046    /// Returns the emitted body and import flags without building the preamble.
5047    fn into_parts(self) -> (String, GoImportNeeds) {
5048        (
5049            self.buf,
5050            GoImportNeeds {
5051                fmt: self.needs_fmt_import,
5052                sync: self.needs_sync_import,
5053                time: self.needs_time_import,
5054                strings: self.needs_strings_import,
5055                utf8: self.needs_utf8_import,
5056                math: self.needs_math_import,
5057                unicode: self.needs_unicode_import,
5058                strconv: self.needs_strconv_import,
5059                reflect: self.needs_reflect_import,
5060            },
5061        )
5062    }
5063
5064    fn finish(self) -> String {
5065        let mut header = format!("package {}\n", self.package_name);
5066        let needs = GoImportNeeds {
5067            fmt: self.needs_fmt_import,
5068            sync: self.needs_sync_import,
5069            time: self.needs_time_import,
5070            strings: self.needs_strings_import,
5071            utf8: self.needs_utf8_import,
5072            math: self.needs_math_import,
5073            unicode: self.needs_unicode_import,
5074            strconv: self.needs_strconv_import,
5075            reflect: self.needs_reflect_import,
5076        };
5077        header.push_str(&needs.render_block());
5078        header.push('\n');
5079        header.push_str(&self.buf);
5080        header
5081    }
5082
5083    fn indent_str(&self) -> String {
5084        "\t".repeat(self.indent)
5085    }
5086
5087    fn write_indent(&mut self) {
5088        let indent = self.indent_str();
5089        self.buf.push_str(&indent);
5090    }
5091
5092    fn writeln(&mut self, s: &str) {
5093        self.write_indent();
5094        self.buf.push_str(s);
5095        self.buf.push('\n');
5096    }
5097
5098    // ── Prelude function mapping ──────────────────────────────────────────
5099
5100    /// Emit an expression into a temporary buffer and return the string.
5101    fn expr_to_string(&mut self, node: &AIRNode) -> Result<String, CodegenError> {
5102        let start = self.buf.len();
5103        self.emit_expr(node)?;
5104        let s = self.buf[start..].to_string();
5105        self.buf.truncate(start);
5106        Ok(s)
5107    }
5108
5109    /// The Go string for an `Optional`/`Result` constructor's payload, casting a
5110    /// *numeric literal* payload to its concrete Go type (`int64` / `float64`).
5111    ///
5112    /// The runtime boxes the payload as `interface{}`. A bare Go integer literal
5113    /// (`__bockSome(7)`) is an *untyped constant* whose default boxed dynamic
5114    /// type is Go `int`, not `int64` — so a later `.(int64)` payload assertion
5115    /// (or a generic `.(T)` with `T` instantiated as `int64`) panics
5116    /// `interface {} is int, not int64`. The read-side widening helpers
5117    /// (`__bockAsInt64`) mask this for *concrete* `int64` assertions, but a
5118    /// generic free fn's body asserts the bare type parameter `T`, which has no
5119    /// widening. Boxing the literal as `int64(7)` / `float64(..)` makes the
5120    /// dynamic type match the instantiation, so both assertion forms succeed.
5121    /// Non-literal and non-numeric payloads are passed through unchanged.
5122    fn box_payload_str(&self, arg: Option<&bock_air::AirArg>, arg_strs: &[String]) -> String {
5123        let rendered = arg_strs
5124            .first()
5125            .map_or_else(|| "nil".to_string(), |s| s.clone());
5126        let Some(arg) = arg else {
5127            return rendered;
5128        };
5129        match Self::numeric_literal_go_type(&arg.value) {
5130            Some(go_ty) => format!("{go_ty}({rendered})"),
5131            None => rendered,
5132        }
5133    }
5134
5135    /// The Go numeric type (`int64`/`float64`) of an expression that is a numeric
5136    /// literal — directly, or under a unary negation (`-1`) — else `None`. Used
5137    /// to box an `Optional`/`Result` payload literal at its concrete dynamic type
5138    /// (see [`Self::box_payload_str`]).
5139    /// Decide whether a `**` (`BinOp::Pow`) should lower to the floating-point
5140    /// path (`math.Pow`) or the integer path (`__bockIntPow`). Returns `true` if
5141    /// either operand is statically float-typed (a `Float` literal, or a binding
5142    /// inferred to `float64`/`float32`). When neither operand resolves to a float
5143    /// — the common `2 ** 10` integer case, or an unresolved operand — the integer
5144    /// helper is chosen, which keeps exact integer precision. Both operands are
5145    /// coerced to the chosen numeric type at the call site, so a mixed
5146    /// `Int ** Float` still routes to `math.Pow` and type-checks.
5147    fn pow_is_float(&self, left: &AIRNode, right: &AIRNode) -> bool {
5148        let is_float = |this: &Self, n: &AIRNode| -> bool {
5149            matches!(
5150                this.infer_go_expr_type(n).as_deref(),
5151                Some("float64") | Some("float32")
5152            )
5153        };
5154        is_float(self, left) || is_float(self, right)
5155    }
5156
5157    fn numeric_literal_go_type(node: &AIRNode) -> Option<&'static str> {
5158        match &node.kind {
5159            NodeKind::Literal { lit } => match lit {
5160                Literal::Int(_) => Some("int64"),
5161                Literal::Float(_) => Some("float64"),
5162                _ => None,
5163            },
5164            NodeKind::UnaryOp {
5165                op: UnaryOp::Neg,
5166                operand,
5167            } => Self::numeric_literal_go_type(operand),
5168            _ => None,
5169        }
5170    }
5171
5172    /// Map Bock prelude functions to Go equivalents.
5173    fn map_prelude_call(
5174        &mut self,
5175        callee: &AIRNode,
5176        args: &[bock_air::AirArg],
5177    ) -> Result<Option<String>, CodegenError> {
5178        let name = match &callee.kind {
5179            NodeKind::Identifier { name } => name.name.as_str(),
5180            _ => return Ok(None),
5181        };
5182        let arg_strs: Vec<String> = args
5183            .iter()
5184            .map(|a| self.expr_to_string(&a.value))
5185            .collect::<Result<_, _>>()?;
5186        let code = match name {
5187            "println" => {
5188                self.needs_fmt_import = true;
5189                let a = arg_strs.first().map_or(String::new(), |s| s.clone());
5190                format!("fmt.Println({a})")
5191            }
5192            "print" => {
5193                self.needs_fmt_import = true;
5194                let a = arg_strs.first().map_or(String::new(), |s| s.clone());
5195                format!("fmt.Print({a})")
5196            }
5197            "debug" => {
5198                self.needs_fmt_import = true;
5199                let a = arg_strs.first().map_or(String::new(), |s| s.clone());
5200                format!("fmt.Printf(\"%+v\\n\", {a})")
5201            }
5202            "assert" => {
5203                let a = arg_strs.first().map_or(String::new(), |s| s.clone());
5204                format!("if !{a} {{ panic(\"assertion failed\") }}")
5205            }
5206            "todo" => "panic(\"not implemented\")".to_string(),
5207            "unreachable" => "panic(\"unreachable\")".to_string(),
5208            "sleep" => {
5209                let a = arg_strs.first().map_or(String::new(), |s| s.clone());
5210                // Route through an installed `Clock` handler if one is in scope;
5211                // otherwise fall through to the host primitive (default).
5212                if let Some(handler) = self.clock_handler_var() {
5213                    format!("{handler}.{}({a})", to_pascal_case("sleep"))
5214                } else {
5215                    // sleep(d) returns a chan struct{} so `await` (= `<-ch`)
5216                    // works uniformly. The goroutine holds for `d` nanos, then
5217                    // closes ch.
5218                    self.needs_time_import = true;
5219                    format!("(func() <-chan struct{{}} {{ __ch := make(chan struct{{}}); go func() {{ time.Sleep(time.Duration({a})); close(__ch) }}(); return __ch }})()")
5220                }
5221            }
5222            // Optional constructors → tagged runtime struct.
5223            "Some" => {
5224                let a = self.box_payload_str(args.first(), &arg_strs);
5225                format!("__bockSome({a})")
5226            }
5227            "None" => "__bockNone".to_string(),
5228            // Result constructors → tagged runtime struct (see
5229            // `RESULT_RUNTIME_GO`), mirroring `Some`/`None`.
5230            "Ok" => {
5231                let a = self.box_payload_str(args.first(), &arg_strs);
5232                format!("__bockOk({a})")
5233            }
5234            "Err" => {
5235                let a = self.box_payload_str(args.first(), &arg_strs);
5236                format!("__bockErr({a})")
5237            }
5238            _ => return Ok(None),
5239        };
5240        Ok(Some(code))
5241    }
5242
5243    /// Emit a built-in `Optional`/`Result` method call to its Go form.
5244    ///
5245    /// Recognised via the checker's `recv_kind` annotation
5246    /// ([`crate::generator::desugared_optional_method`] /
5247    /// [`crate::generator::desugared_result_method`]). The tagged runtime structs
5248    /// (`__bockOption`/`__bockResult`) carry the payload as `interface{}` in `.v`
5249    /// and the tag in `.tag`, so a method lowers to a Go closure IIFE that tests
5250    /// `.tag` and recovers the payload. The payload Go type (for `unwrap`/
5251    /// `unwrap_or`) is resolved from the receiver's declared `Optional[T]` /
5252    /// `Result[T, E]` type; when unknown it stays `interface{}` (works for `%v`
5253    /// interpolation, the conservative fallback the Optional match also uses).
5254    /// Returns `true` if handled.
5255    fn try_emit_container_method(
5256        &mut self,
5257        node: &AIRNode,
5258        callee: &AIRNode,
5259        args: &[bock_air::AirArg],
5260    ) -> Result<bool, CodegenError> {
5261        if let Some((recv, method, rest)) =
5262            crate::generator::desugared_optional_method(node, callee, args)
5263        {
5264            let elem = self.scrutinee_optional_elem(recv);
5265            self.emit_tagged_container_method(
5266                recv,
5267                method,
5268                rest,
5269                "Some",
5270                "__bockSome",
5271                "__bockNone",
5272                elem.as_deref(),
5273            )?;
5274            return Ok(true);
5275        }
5276        if let Some((recv, method, rest)) =
5277            crate::generator::desugared_result_method(node, callee, args)
5278        {
5279            let elems = self.scrutinee_result_elems(recv);
5280            let ok = elems.as_ref().map(|(o, _)| o.as_str());
5281            self.emit_tagged_container_method(
5282                recv,
5283                method,
5284                rest,
5285                "Ok",
5286                "__bockOk",
5287                "__bockErr",
5288                ok,
5289            )?;
5290            return Ok(true);
5291        }
5292        Ok(false)
5293    }
5294
5295    /// Lower a tagged-container method on `recv` to a Go closure IIFE.
5296    /// `present_tag` is the payload-carrying tag (`"Some"`/`"Ok"`);
5297    /// `present_ctor`/`other_ctor` are the runtime constructors; `payload_ty` is
5298    /// the Go type the payload is asserted to (`None` → bare `interface{}`).
5299    #[allow(clippy::too_many_arguments)]
5300    fn emit_tagged_container_method(
5301        &mut self,
5302        recv: &AIRNode,
5303        method: &str,
5304        rest: &[bock_air::AirArg],
5305        present_tag: &str,
5306        present_ctor: &str,
5307        other_ctor: &str,
5308        payload_ty: Option<&str>,
5309    ) -> Result<(), CodegenError> {
5310        // The closure binds the receiver once as `__c` (a tagged struct).
5311        // Tag tests: `is_some`/`is_ok` and `is_none`/`is_err`.
5312        match method {
5313            "is_some" | "is_ok" => {
5314                self.buf.push('(');
5315                self.emit_expr(recv)?;
5316                let _ = write!(self.buf, ".tag == \"{present_tag}\")");
5317                return Ok(());
5318            }
5319            "is_none" | "is_err" => {
5320                self.buf.push('(');
5321                self.emit_expr(recv)?;
5322                let _ = write!(self.buf, ".tag != \"{present_tag}\")");
5323                return Ok(());
5324            }
5325            _ => {}
5326        }
5327        // Recover the payload as its concrete type (numeric boxings widened via
5328        // the shared helpers; otherwise a type assertion; else bare `.v`).
5329        let payload_expr = |ty: Option<&str>| -> String {
5330            match ty {
5331                Some("int64") => "__bockAsInt64(__c.v)".to_string(),
5332                Some("float64") => "__bockAsFloat64(__c.v)".to_string(),
5333                Some(t) => format!("__c.v.({t})"),
5334                None => "__c.v".to_string(),
5335            }
5336        };
5337        match method {
5338            "unwrap" | "unwrap_or" => {
5339                let ret_ty = payload_ty.unwrap_or("interface{}");
5340                let payload = payload_expr(payload_ty);
5341                let _ = write!(
5342                    self.buf,
5343                    "func(__c {recv_ty}) {ret_ty} {{ if __c.tag == \"{present_tag}\" {{ return {payload} }}; return ",
5344                    recv_ty = self.container_runtime_ty(present_ctor),
5345                );
5346                if method == "unwrap_or" {
5347                    if let Some(d) = rest.first() {
5348                        self.emit_expr(&d.value)?;
5349                    } else {
5350                        // No default supplied — fall back to the zero value.
5351                        self.zero_value_for(ret_ty);
5352                    }
5353                } else {
5354                    // `unwrap` on the empty case panics (no default supplied).
5355                    self.zero_value_for(ret_ty);
5356                }
5357                self.buf.push_str(" }(");
5358                self.emit_expr(recv)?;
5359                self.buf.push(')');
5360            }
5361            "map" => {
5362                // Apply the callback to the payload and rewrap as the present
5363                // variant; the empty/other variant passes through unchanged.
5364                let recv_ty = self.container_runtime_ty(present_ctor);
5365                let payload = payload_expr(payload_ty);
5366                let _ = write!(
5367                    self.buf,
5368                    "func(__c {recv_ty}) {recv_ty} {{ if __c.tag == \"{present_tag}\" {{ return {present_ctor}("
5369                );
5370                if let Some(f) = rest.first() {
5371                    self.emit_expr(&f.value)?;
5372                } else {
5373                    self.buf
5374                        .push_str("func(x interface{}) interface{} { return x }");
5375                }
5376                let _ = write!(self.buf, "({payload})) }}; return __c }}(");
5377                self.emit_expr(recv)?;
5378                self.buf.push(')');
5379            }
5380            "flat_map" => {
5381                let recv_ty = self.container_runtime_ty(present_ctor);
5382                let payload = payload_expr(payload_ty);
5383                let _ = write!(
5384                    self.buf,
5385                    "func(__c {recv_ty}) {recv_ty} {{ if __c.tag == \"{present_tag}\" {{ return "
5386                );
5387                if let Some(f) = rest.first() {
5388                    self.emit_expr(&f.value)?;
5389                } else {
5390                    self.buf
5391                        .push_str("func(x interface{}) interface{} { return x }");
5392                }
5393                let _ = write!(self.buf, "({payload}).({recv_ty}) }}; return __c }}(");
5394                self.emit_expr(recv)?;
5395                self.buf.push(')');
5396            }
5397            "map_err" => {
5398                let recv_ty = self.container_runtime_ty(present_ctor);
5399                let _ = write!(
5400                    self.buf,
5401                    "func(__c {recv_ty}) {recv_ty} {{ if __c.tag != \"{present_tag}\" {{ return {other_ctor}("
5402                );
5403                if let Some(f) = rest.first() {
5404                    self.emit_expr(&f.value)?;
5405                } else {
5406                    self.buf
5407                        .push_str("func(x interface{}) interface{} { return x }");
5408                }
5409                self.buf.push_str("(__c.v)) }; return __c }(");
5410                self.emit_expr(recv)?;
5411                self.buf.push(')');
5412            }
5413            _ => {
5414                self.buf.push_str("nil");
5415            }
5416        }
5417        Ok(())
5418    }
5419
5420    /// The Go runtime struct type for a container, keyed by its present-variant
5421    /// constructor (`__bockSome` → `__bockOption`, `__bockOk` → `__bockResult`).
5422    fn container_runtime_ty(&self, present_ctor: &str) -> &'static str {
5423        if present_ctor == "__bockOk" {
5424            "__bockResult"
5425        } else {
5426            "__bockOption"
5427        }
5428    }
5429
5430    /// Emit a Go zero value for `ty` (used as the `unwrap`-on-empty fallback).
5431    fn zero_value_for(&mut self, ty: &str) {
5432        let zero = match ty {
5433            "int64" | "float64" | "int" | "float32" | "int32" => "0",
5434            "string" => "\"\"",
5435            "bool" => "false",
5436            _ => "nil",
5437        };
5438        self.buf.push_str(zero);
5439    }
5440
5441    /// Emit a read-only `List` built-in method call to its Go form.
5442    ///
5443    /// Lists are `[]interface{}`. `len`/`length`/`count` wrap in `int64(...)`;
5444    /// `is_empty` compares the length. `Optional`-returning methods
5445    /// (`get`/`first`/`last`/`index_of`) build the tagged Optional runtime
5446    /// (`__bockSome(v)` / `__bockNone`) inside an immediately-called closure so
5447    /// the receiver is evaluated once and bounds are checked. `contains` /
5448    /// `index_of` / `concat` / `join` use inline closures (no top-level helper
5449    /// injection needed). The `__bockSome` payload is `interface{}`; a `match`
5450    /// arm binding it re-asserts the element type via the existing Optional
5451    /// resolver (`scrutinee_optional_elem`), which now resolves
5452    /// `get`/`first`/`last` on a typed `List[T]` receiver.
5453    fn try_emit_list_method(
5454        &mut self,
5455        node: &AIRNode,
5456        callee: &AIRNode,
5457        args: &[bock_air::AirArg],
5458    ) -> Result<bool, CodegenError> {
5459        let Some((recv, method, rest)) =
5460            crate::generator::desugared_list_method(node, callee, args)
5461        else {
5462            return Ok(false);
5463        };
5464        let recv_str = self.expr_to_string(recv)?;
5465        // The receiver's Go slice element type. Lists are now concretely typed
5466        // (`[]int64`, etc.), so the closure parameter type (`__r []<elem>`) must
5467        // match the receiver — a `[]int64` argument does NOT convert to a
5468        // `[]interface{}` parameter in Go. When the element type can't be
5469        // recovered the receiver is still `[]interface{}` (the literal/inference
5470        // fallback), so `interface{}` is the correct, matching default.
5471        let elem = self
5472            .list_receiver_elem_go_type(recv)
5473            .unwrap_or_else(|| "interface{}".to_string());
5474        let slice = format!("[]{elem}");
5475        let code = match method {
5476            "len" | "length" | "count" => format!("int64(len({recv_str}))"),
5477            "is_empty" => format!("(len({recv_str}) == 0)"),
5478            "get" => {
5479                let Some(idx) = rest.first() else {
5480                    return Ok(false);
5481                };
5482                let i = self.expr_to_string(&idx.value)?;
5483                format!(
5484                    "func(__r {slice}, __i int64) __bockOption {{ \
5485                     if __i >= 0 && __i < int64(len(__r)) {{ return __bockSome(__r[__i]) }}; \
5486                     return __bockNone }}({recv_str}, {i})"
5487                )
5488            }
5489            "first" => format!(
5490                "func(__r {slice}) __bockOption {{ \
5491                 if len(__r) > 0 {{ return __bockSome(__r[0]) }}; \
5492                 return __bockNone }}({recv_str})"
5493            ),
5494            "last" => format!(
5495                "func(__r {slice}) __bockOption {{ \
5496                 if len(__r) > 0 {{ return __bockSome(__r[len(__r)-1]) }}; \
5497                 return __bockNone }}({recv_str})"
5498            ),
5499            "contains" => {
5500                let Some(x) = rest.first() else {
5501                    return Ok(false);
5502                };
5503                self.needs_fmt_import = true;
5504                let x = self.expr_to_string(&x.value)?;
5505                // Compare on the `%v` string form, not raw `interface{}` `==`:
5506                // a list literal boxes Go `int`/`float64` while a typed `Int`
5507                // variable is `int64`, so `int(30) == int64(30)` is *false*
5508                // under Go's type-and-value interface equality. The checker
5509                // guarantees `contains(x: T)` on `List[T]` (same T), so the two
5510                // operands always denote the same Bock type — `%v` normalises
5511                // only the int/int64 boxing difference. `__x` stays
5512                // `interface{}` (a typed argument boxes into it).
5513                format!(
5514                    "func(__r {slice}, __x interface{{}}) bool {{ \
5515                     __xs := fmt.Sprintf(\"%v\", __x); \
5516                     for _, __e := range __r {{ if fmt.Sprintf(\"%v\", __e) == __xs {{ return true }} }}; \
5517                     return false }}({recv_str}, {x})"
5518                )
5519            }
5520            "index_of" => {
5521                let Some(x) = rest.first() else {
5522                    return Ok(false);
5523                };
5524                self.needs_fmt_import = true;
5525                let x = self.expr_to_string(&x.value)?;
5526                // See `contains` for why this compares `%v` forms, not `==`.
5527                format!(
5528                    "func(__r {slice}, __x interface{{}}) __bockOption {{ \
5529                     __xs := fmt.Sprintf(\"%v\", __x); \
5530                     for __i, __e := range __r {{ if fmt.Sprintf(\"%v\", __e) == __xs {{ return __bockSome(int64(__i)) }} }}; \
5531                     return __bockNone }}({recv_str}, {x})"
5532                )
5533            }
5534            "concat" => {
5535                let Some(o) = rest.first() else {
5536                    return Ok(false);
5537                };
5538                // The `__o` IIFE parameter is `[]elem` (the receiver's element
5539                // type), so the argument list literal must also be `[]elem{...}`
5540                // — a `[]interface{}{x}` argument is not assignable to a `[]T`
5541                // parameter in Go. Thread the receiver's element type into the
5542                // literal as its expected collection element (extends #144's
5543                // return-position typed-literal fix to argument position).
5544                let prev_expected = self.expected_collection_elem.take();
5545                if matches!(
5546                    o.value.kind,
5547                    NodeKind::ListLiteral { .. }
5548                        | NodeKind::MapLiteral { .. }
5549                        | NodeKind::SetLiteral { .. }
5550                ) {
5551                    self.expected_collection_elem = Some((elem.clone(), None));
5552                }
5553                let o = self.expr_to_string(&o.value)?;
5554                self.expected_collection_elem = prev_expected;
5555                format!(
5556                    "func(__r {slice}, __o {slice}) {slice} {{ \
5557                     __v := make({slice}, 0, len(__r)+len(__o)); \
5558                     __v = append(__v, __r...); __v = append(__v, __o...); \
5559                     return __v }}({recv_str}, {o})"
5560                )
5561            }
5562            "join" => {
5563                let Some(sep) = rest.first() else {
5564                    return Ok(false);
5565                };
5566                self.needs_fmt_import = true;
5567                let sep = self.expr_to_string(&sep.value)?;
5568                format!(
5569                    "func(__r {slice}, __sep string) string {{ \
5570                     __s := \"\"; \
5571                     for __i, __e := range __r {{ if __i > 0 {{ __s += __sep }}; \
5572                     __s += fmt.Sprintf(\"%v\", __e) }}; \
5573                     return __s }}({recv_str}, {sep})"
5574                )
5575            }
5576            _ => return Ok(false),
5577        };
5578        self.buf.push_str(&code);
5579        Ok(true)
5580    }
5581
5582    /// Emit an in-place `List` mutator (`push`/`append`, DQ18) in **statement
5583    /// position** to its Go form.
5584    ///
5585    /// Recognised via [`crate::generator::desugared_list_mutating_method`]. Go
5586    /// grows a slice by *reassignment* — `recv = append(recv, x)` — so unlike the
5587    /// other backends (which emit a value-less `recv.push(x)`) this is an
5588    /// assignment statement, emitted only from `emit_stmt`. The checker types
5589    /// `push`/`append` as `Void`, so the call always appears in statement
5590    /// position, and the ownership pass guarantees the receiver is a `mut` lvalue
5591    /// (a `let mut` slice, a `mut` parameter, or a field reachable through a
5592    /// `mut` receiver), so the same place expression is a valid assignment
5593    /// target on the left of `=`. A field receiver lowers to its Go-cased place
5594    /// (`r.Items = append(r.Items, x)`) via `expr_to_string`.
5595    ///
5596    /// Returns `false` (no statement emitted) when the call is not a recognised
5597    /// in-place `List` mutator, so the caller falls back to the generic
5598    /// expression-statement path.
5599    fn try_emit_list_mutating_stmt(
5600        &mut self,
5601        node: &AIRNode,
5602        callee: &AIRNode,
5603        args: &[bock_air::AirArg],
5604    ) -> Result<bool, CodegenError> {
5605        let Some((recv, _method, rest)) =
5606            crate::generator::desugared_list_mutating_method(node, callee, args)
5607        else {
5608            return Ok(false);
5609        };
5610        let Some(x) = rest.first() else {
5611            return Ok(false);
5612        };
5613        let recv_str = self.expr_to_string(recv)?;
5614        let x = self.expr_to_string(&x.value)?;
5615        self.write_indent();
5616        let _ = write!(self.buf, "{recv_str} = append({recv_str}, {x})");
5617        self.buf.push('\n');
5618        Ok(true)
5619    }
5620
5621    /// Emit a DQ30 in-place `List` mutator
5622    /// (`pop`/`remove_at`/`insert`/`reverse`/`set`) to its Go form.
5623    ///
5624    /// Recognised via [`crate::generator::desugared_list_inplace_mutator`]. Go
5625    /// slices are *values* (a length change must reassign the receiver place,
5626    /// the DQ18 `recv = append(recv, x)` pattern), but `pop`/`remove_at` also
5627    /// *return* a value, so they cannot be assignment statements. The two are
5628    /// reconciled by spelling the reassign through a **pointer**: each
5629    /// length-changing lowering is an immediately-invoked func literal taking
5630    /// `__r *[]T` and assigning `*__r = …` — the same receiver reassignment,
5631    /// composed into expression position. The ownership pass guarantees the
5632    /// receiver is a `mut` lvalue (an identifier / field / index place), so
5633    /// `&recv` is addressable. `reverse` and `set` never change the length, so
5634    /// they mutate the shared backing array directly (no pointer):
5635    ///
5636    /// - `pop` → len-check + last-element grab + `*__r = (*__r)[:len-1]`
5637    ///   shrink, wrapped into the tagged `__bockOption` runtime
5638    ///   (`__bockSome(v)` / `__bockNone`);
5639    /// - `remove_at(i)` → bounds-check + grab +
5640    ///   `*__r = append((*__r)[:i], (*__r)[i+1:]...)` reassign;
5641    /// - `insert(i, x)` → bounds-check (`0..=len`) + append-grow +
5642    ///   `copy` right-shift + write;
5643    /// - `reverse` → in-place two-index swap loop;
5644    /// - `set(i, x)` → bounds-check + `__r[i] = x`.
5645    ///
5646    /// The bounds checks `panic(fmt.Sprintf(...))` with the normalized abort
5647    /// message `List.<op>: index <i> out of bounds (len <n>)`, Go's idiomatic
5648    /// abort channel (the DQ23 convention — Go's native divide-by-zero panic
5649    /// was likewise kept).
5650    fn try_emit_list_inplace_mutator(
5651        &mut self,
5652        node: &AIRNode,
5653        callee: &AIRNode,
5654        args: &[bock_air::AirArg],
5655    ) -> Result<bool, CodegenError> {
5656        let Some((recv, method, rest)) =
5657            crate::generator::desugared_list_inplace_mutator(node, callee, args)
5658        else {
5659            return Ok(false);
5660        };
5661        let recv_str = self.expr_to_string(recv)?;
5662        let elem = self
5663            .list_receiver_elem_go_type(recv)
5664            .unwrap_or_else(|| "interface{}".to_string());
5665        let slice = format!("[]{elem}");
5666        let code = match method {
5667            "pop" => format!(
5668                "func(__r *{slice}) __bockOption {{ \
5669                 if len(*__r) == 0 {{ return __bockNone }}; \
5670                 __v := (*__r)[len(*__r)-1]; \
5671                 *__r = (*__r)[:len(*__r)-1]; \
5672                 return __bockSome(__v) }}(&{recv_str})"
5673            ),
5674            "remove_at" => {
5675                let Some(idx) = rest.first() else {
5676                    return Ok(false);
5677                };
5678                self.needs_fmt_import = true;
5679                let i = self.expr_to_string(&idx.value)?;
5680                format!(
5681                    "func(__r *{slice}, __i int64) {elem} {{ \
5682                     if __i < 0 || __i >= int64(len(*__r)) {{ \
5683                     panic(fmt.Sprintf(\"List.remove_at: index %d out of bounds (len %d)\", __i, len(*__r))) }}; \
5684                     __v := (*__r)[__i]; \
5685                     *__r = append((*__r)[:__i], (*__r)[__i+1:]...); \
5686                     return __v }}(&{recv_str}, {i})"
5687                )
5688            }
5689            "insert" => {
5690                let (Some(idx), Some(x)) = (rest.first(), rest.get(1)) else {
5691                    return Ok(false);
5692                };
5693                self.needs_fmt_import = true;
5694                let i = self.expr_to_string(&idx.value)?;
5695                let x = self.expr_to_string(&x.value)?;
5696                format!(
5697                    "func(__r *{slice}, __i int64, __x {elem}) {{ \
5698                     if __i < 0 || __i > int64(len(*__r)) {{ \
5699                     panic(fmt.Sprintf(\"List.insert: index %d out of bounds (len %d)\", __i, len(*__r))) }}; \
5700                     *__r = append(*__r, __x); \
5701                     copy((*__r)[__i+1:], (*__r)[__i:]); \
5702                     (*__r)[__i] = __x }}(&{recv_str}, {i}, {x})"
5703                )
5704            }
5705            "reverse" => format!(
5706                "func(__r {slice}) {{ \
5707                 for __i, __j := 0, len(__r)-1; __i < __j; __i, __j = __i+1, __j-1 {{ \
5708                 __r[__i], __r[__j] = __r[__j], __r[__i] }} }}({recv_str})"
5709            ),
5710            "set" => {
5711                let (Some(idx), Some(x)) = (rest.first(), rest.get(1)) else {
5712                    return Ok(false);
5713                };
5714                self.needs_fmt_import = true;
5715                let i = self.expr_to_string(&idx.value)?;
5716                let x = self.expr_to_string(&x.value)?;
5717                format!(
5718                    "func(__r {slice}, __i int64, __x {elem}) {{ \
5719                     if __i < 0 || __i >= int64(len(__r)) {{ \
5720                     panic(fmt.Sprintf(\"List.set: index %d out of bounds (len %d)\", __i, len(__r))) }}; \
5721                     __r[__i] = __x }}({recv_str}, {i}, {x})"
5722                )
5723            }
5724            _ => return Ok(false),
5725        };
5726        self.buf.push_str(&code);
5727        Ok(true)
5728    }
5729
5730    /// Render a closure (lambda) argument as a typed Go func literal, with its
5731    /// parameter types pinned to `param_types`, and report the func literal's
5732    /// inferred return type.
5733    ///
5734    /// Used by [`Self::try_emit_list_functional_method`]: a bare `(x) => x * 2`
5735    /// argument would otherwise emit `func(x interface{}) interface{}`, whose
5736    /// return does not assign into a concrete `[]int64`. Pinning the params to the
5737    /// receiver's element type (`[]int64` → `int64`) lets the Go backend infer a
5738    /// concrete return type, which the caller uses both to size the result slice
5739    /// and to decide whether a type assertion is needed.
5740    fn render_typed_closure_go(
5741        &mut self,
5742        closure: &AIRNode,
5743        param_types: &[String],
5744    ) -> Result<(String, Option<String>), CodegenError> {
5745        self.render_typed_closure_go_ret(closure, param_types, None)
5746    }
5747
5748    /// As [`Self::render_typed_closure_go`], but a `forced_ret` of `Some(ty)` pins
5749    /// the closure's Go return type (a predicate combinator's `bool`) rather than
5750    /// inferring it from the body. The returned `Option<String>` ret reflects the
5751    /// forced type when given, so the caller's result-elem logic agrees.
5752    fn render_typed_closure_go_ret(
5753        &mut self,
5754        closure: &AIRNode,
5755        param_types: &[String],
5756        forced_ret: Option<&str>,
5757    ) -> Result<(String, Option<String>), CodegenError> {
5758        let ret = if let Some(t) = forced_ret {
5759            Some(t.to_string())
5760        } else if let NodeKind::Lambda { params, body } = &closure.kind {
5761            let saved = self.enter_param_go_types_with_expected(params, Some(param_types));
5762            // Use the block-tail inference (not the bare-expr one): a `.map`
5763            // closure whose body is a block / `if` / `match` (e.g.
5764            // `(t) => { if (t.id == id) { t.complete() } else { t } }`) needs its
5765            // value tail typed so the result slice is `[]Todo`, not `[]interface{}`
5766            // (`infer_go_expr_type` alone returns `None` for a block/if body).
5767            let r = self.infer_block_tail_type(body);
5768            self.var_go_type = saved;
5769            r
5770        } else {
5771            None
5772        };
5773        let prev = self.expected_lambda_param_types.take();
5774        self.expected_lambda_param_types = Some(param_types.to_vec());
5775        let prev_forced = self.forced_lambda_ret.take();
5776        self.forced_lambda_ret = forced_ret.map(str::to_string);
5777        let code = self.expr_to_string(closure)?;
5778        self.forced_lambda_ret = prev_forced;
5779        self.expected_lambda_param_types = prev;
5780        Ok((code, ret))
5781    }
5782
5783    /// The Go element type to use for a `List` combinator's *result* slice, drawn
5784    /// from the binding's expected type (`current_expected_type`, e.g. `[]string`
5785    /// → `string`) when it is a slice, else from the closure's inferred return
5786    /// `cb_ret`, else `interface{}`.
5787    fn list_result_elem_go(&self, cb_ret: Option<&str>) -> String {
5788        if let Some(t) = self.current_expected_type.as_deref() {
5789            if let Some(elem) = t.strip_prefix("[]") {
5790                return elem.to_string();
5791            }
5792        }
5793        cb_ret.unwrap_or("interface{}").to_string()
5794    }
5795
5796    /// Emit a functional (closure-taking) `List` built-in method call to its Go
5797    /// form.
5798    ///
5799    /// Recognised via [`crate::generator::desugared_list_functional_method`]. Go
5800    /// has neither methods on slices nor a usable `map` selector (`map` is a
5801    /// keyword), so each combinator lowers to an immediately-invoked func literal
5802    /// that drives a `for _, __x := range __r` loop, applying the closure
5803    /// (rendered with its parameter types pinned to the receiver's element type,
5804    /// via [`Self::render_typed_closure_go`]). The closure is evaluated *once* —
5805    /// the desugared `recv.map(recv, cb)` shape the generic fall-through emits
5806    /// otherwise produces `expected selector …, found 'map'` (a parse error,
5807    /// `map` being reserved) or `.filter undefined`. `map`/`flat_map` size their
5808    /// result from the binding's expected element type; `fold`/`reduce` thread an
5809    /// accumulator; `find` returns the tagged Optional runtime; `any`/`all`
5810    /// short-circuit to `bool`; `for_each` returns nothing.
5811    fn try_emit_list_functional_method(
5812        &mut self,
5813        node: &AIRNode,
5814        callee: &AIRNode,
5815        args: &[bock_air::AirArg],
5816    ) -> Result<bool, CodegenError> {
5817        let Some((recv, method, rest)) =
5818            crate::generator::desugared_list_functional_method(node, callee, args)
5819        else {
5820            return Ok(false);
5821        };
5822        let recv_str = self.expr_to_string(recv)?;
5823        // Recover the receiver's element type. `list_receiver_elem_go_type`
5824        // handles direct bindings/literals and the element-preserving chained
5825        // combinators (`filter`/`find`). A chained `map`/`flat_map` receiver's
5826        // element is the *closure's return type*, which needs the `&mut self`
5827        // block-tail inference in `value_list_elem_go_type` — so fall back to it
5828        // when the cheap `&self` resolver yields nothing. This is the
5829        // `.filter(..).map(..).map(..)` (Q-go-chained-combinator-typing) case:
5830        // the outermost `.map`'s `interface{}` element flips to the concrete
5831        // element threaded through the whole chain.
5832        let elem = self
5833            .list_receiver_elem_go_type(recv)
5834            .or_else(|| self.value_list_elem_go_type(recv))
5835            .unwrap_or_else(|| "interface{}".to_string());
5836        let slice = format!("[]{elem}");
5837        let code = match method {
5838            "map" => {
5839                let Some(cb) = rest.first() else {
5840                    return Ok(false);
5841                };
5842                let (f, cb_ret) =
5843                    self.render_typed_closure_go(&cb.value, std::slice::from_ref(&elem))?;
5844                let out = self.list_result_elem_go(cb_ret.as_deref());
5845                // Assert the closure result into the declared element type unless
5846                // the closure's inferred return type *already* matches it exactly
5847                // (asserting a concrete value onto its own type is a Go compile
5848                // error; appending an `interface{}` to a concrete `[]T` without
5849                // an assertion is also an error). When `out` is `interface{}` no
5850                // assertion is needed (anything assigns to `interface{}`).
5851                let push = if out != "interface{}" && cb_ret.as_deref() != Some(out.as_str()) {
5852                    format!("__out = append(__out, __f(__x).({out}))")
5853                } else {
5854                    "__out = append(__out, __f(__x))".to_string()
5855                };
5856                format!(
5857                    "func(__r {slice}) []{out} {{ \
5858                     __f := {f}; \
5859                     __out := make([]{out}, 0, len(__r)); \
5860                     for _, __x := range __r {{ {push} }}; \
5861                     return __out }}({recv_str})"
5862                )
5863            }
5864            "flat_map" => {
5865                let Some(cb) = rest.first() else {
5866                    return Ok(false);
5867                };
5868                let (f, cb_ret) =
5869                    self.render_typed_closure_go(&cb.value, std::slice::from_ref(&elem))?;
5870                // The closure returns a slice; its element is the result element.
5871                let out = self.current_expected_type.as_deref().map_or_else(
5872                    || {
5873                        cb_ret
5874                            .as_deref()
5875                            .and_then(|r| r.strip_prefix("[]"))
5876                            .unwrap_or("interface{}")
5877                            .to_string()
5878                    },
5879                    |t| t.strip_prefix("[]").unwrap_or("interface{}").to_string(),
5880                );
5881                format!(
5882                    "func(__r {slice}) []{out} {{ \
5883                     __f := {f}; \
5884                     __out := make([]{out}, 0, len(__r)); \
5885                     for _, __x := range __r {{ __out = append(__out, __f(__x)...) }}; \
5886                     return __out }}({recv_str})"
5887                )
5888            }
5889            "filter" => {
5890                let Some(cb) = rest.first() else {
5891                    return Ok(false);
5892                };
5893                // The predicate returns `Bool`; pin it so `if __f(__x)` is a Go
5894                // boolean condition (the body may be a method call / `match` whose
5895                // Go return type is otherwise erased to `interface{}`).
5896                let (f, _) = self.render_typed_closure_go_ret(
5897                    &cb.value,
5898                    std::slice::from_ref(&elem),
5899                    Some("bool"),
5900                )?;
5901                format!(
5902                    "func(__r {slice}) {slice} {{ \
5903                     __f := {f}; \
5904                     __out := make({slice}, 0, len(__r)); \
5905                     for _, __x := range __r {{ if __f(__x) {{ __out = append(__out, __x) }} }}; \
5906                     return __out }}({recv_str})"
5907                )
5908            }
5909            "find" => {
5910                let Some(cb) = rest.first() else {
5911                    return Ok(false);
5912                };
5913                let (f, _) = self.render_typed_closure_go_ret(
5914                    &cb.value,
5915                    std::slice::from_ref(&elem),
5916                    Some("bool"),
5917                )?;
5918                format!(
5919                    "func(__r {slice}) __bockOption {{ \
5920                     __f := {f}; \
5921                     for _, __x := range __r {{ if __f(__x) {{ return __bockSome(__x) }} }}; \
5922                     return __bockNone }}({recv_str})"
5923                )
5924            }
5925            "any" | "all" => {
5926                let Some(cb) = rest.first() else {
5927                    return Ok(false);
5928                };
5929                let (f, _) = self.render_typed_closure_go_ret(
5930                    &cb.value,
5931                    std::slice::from_ref(&elem),
5932                    Some("bool"),
5933                )?;
5934                // `any`: true if some element matches; `all`: true unless one fails.
5935                if method == "any" {
5936                    format!(
5937                        "func(__r {slice}) bool {{ \
5938                         __f := {f}; \
5939                         for _, __x := range __r {{ if __f(__x) {{ return true }} }}; \
5940                         return false }}({recv_str})"
5941                    )
5942                } else {
5943                    format!(
5944                        "func(__r {slice}) bool {{ \
5945                         __f := {f}; \
5946                         for _, __x := range __r {{ if !__f(__x) {{ return false }} }}; \
5947                         return true }}({recv_str})"
5948                    )
5949                }
5950            }
5951            "reduce" => {
5952                let Some(cb) = rest.first() else {
5953                    return Ok(false);
5954                };
5955                // No seed: first element is the accumulator. Accumulator type is
5956                // the element type.
5957                let (f, _) =
5958                    self.render_typed_closure_go(&cb.value, &[elem.clone(), elem.clone()])?;
5959                format!(
5960                    "func(__r {slice}) {elem} {{ \
5961                     __f := {f}; \
5962                     __acc := __r[0]; \
5963                     for __i := 1; __i < len(__r); __i++ {{ __acc = __f(__acc, __r[__i]) }}; \
5964                     return __acc }}({recv_str})"
5965                )
5966            }
5967            "fold" => {
5968                let (Some(init), Some(cb)) = (rest.first(), rest.get(1)) else {
5969                    return Ok(false);
5970                };
5971                let acc_ty = self
5972                    .infer_go_expr_type(&init.value)
5973                    .or_else(|| self.current_expected_type.clone())
5974                    .unwrap_or_else(|| "interface{}".to_string());
5975                let init_str = self.expr_to_string(&init.value)?;
5976                let (f, _) =
5977                    self.render_typed_closure_go(&cb.value, &[acc_ty.clone(), elem.clone()])?;
5978                format!(
5979                    "func(__r {slice}, __acc {acc_ty}) {acc_ty} {{ \
5980                     __f := {f}; \
5981                     for _, __x := range __r {{ __acc = __f(__acc, __x) }}; \
5982                     return __acc }}({recv_str}, {init_str})"
5983                )
5984            }
5985            "for_each" => {
5986                let Some(cb) = rest.first() else {
5987                    return Ok(false);
5988                };
5989                let (f, _) =
5990                    self.render_typed_closure_go(&cb.value, std::slice::from_ref(&elem))?;
5991                format!(
5992                    "func(__r {slice}) {{ \
5993                     __f := {f}; \
5994                     for _, __x := range __r {{ __f(__x) }} }}({recv_str})"
5995                )
5996            }
5997            _ => return Ok(false),
5998        };
5999        self.buf.push_str(&code);
6000        Ok(true)
6001    }
6002
6003    /// Emit a built-in `Map[K, V]` method call to its Go form (native
6004    /// `map[K]V`, building on P3-α's typed map literals/decls).
6005    ///
6006    /// Recognised via [`crate::generator::desugared_map_method`] (gated on
6007    /// `recv_kind = "Map"`) and wired *before* [`Self::try_emit_list_method`],
6008    /// so a `Map` receiver's `get`/`contains_key`/`len` no longer route through
6009    /// the `List` path (which passed the `map[K]V` where a `[]interface{}` slice
6010    /// closure expected, and cast the key to `int64`). `get` uses the Go
6011    /// comma-ok form (`__v, __ok := __m[__k]`) → the `__bockSome`/`__bockNone`
6012    /// Optional runtime. Mutating methods (`set`/`delete`/`merge`) copy then
6013    /// mutate and return the new map (Bock map value semantics). The inline
6014    /// closures are typed `map[K]V` from the receiver's declared element types
6015    /// (recovered from [`Self::map_receiver_kv_go_types`]; `interface{}`
6016    /// fallback when unknown). Returns `true` if handled.
6017    fn try_emit_map_method(
6018        &mut self,
6019        node: &AIRNode,
6020        callee: &AIRNode,
6021        args: &[bock_air::AirArg],
6022    ) -> Result<bool, CodegenError> {
6023        let Some((recv, method, rest)) = crate::generator::desugared_map_method(node, callee, args)
6024        else {
6025            return Ok(false);
6026        };
6027        let recv_str = self.expr_to_string(recv)?;
6028        let (k_ty, v_ty) = self
6029            .map_receiver_kv_go_types(recv)
6030            .unwrap_or_else(|| ("interface{}".to_string(), "interface{}".to_string()));
6031        let map_ty = format!("map[{k_ty}]{v_ty}");
6032        let code = match method {
6033            "len" | "length" | "count" => format!("int64(len({recv_str}))"),
6034            "is_empty" => format!("(len({recv_str}) == 0)"),
6035            "contains_key" => {
6036                let Some(k) = rest.first() else {
6037                    return Ok(false);
6038                };
6039                let k = self.expr_to_string(&k.value)?;
6040                format!(
6041                    "func(__m {map_ty}, __k {k_ty}) bool {{ _, __ok := __m[__k]; return __ok }}\
6042                     ({recv_str}, {k})"
6043                )
6044            }
6045            "get" => {
6046                let Some(k) = rest.first() else {
6047                    return Ok(false);
6048                };
6049                let k = self.expr_to_string(&k.value)?;
6050                format!(
6051                    "func(__m {map_ty}, __k {k_ty}) __bockOption {{ \
6052                     if __v, __ok := __m[__k]; __ok {{ return __bockSome(__v) }}; \
6053                     return __bockNone }}({recv_str}, {k})"
6054                )
6055            }
6056            "set" => {
6057                let (Some(k), Some(v)) = (rest.first(), rest.get(1)) else {
6058                    return Ok(false);
6059                };
6060                let k = self.expr_to_string(&k.value)?;
6061                let v = self.expr_to_string(&v.value)?;
6062                format!(
6063                    "func(__m {map_ty}, __k {k_ty}, __v {v_ty}) {map_ty} {{ \
6064                     __r := make({map_ty}, len(__m)+1); \
6065                     for __mk, __mv := range __m {{ __r[__mk] = __mv }}; \
6066                     __r[__k] = __v; return __r }}({recv_str}, {k}, {v})"
6067                )
6068            }
6069            "delete" => {
6070                let Some(k) = rest.first() else {
6071                    return Ok(false);
6072                };
6073                let k = self.expr_to_string(&k.value)?;
6074                format!(
6075                    "func(__m {map_ty}, __k {k_ty}) {map_ty} {{ \
6076                     __r := make({map_ty}, len(__m)); \
6077                     for __mk, __mv := range __m {{ __r[__mk] = __mv }}; \
6078                     delete(__r, __k); return __r }}({recv_str}, {k})"
6079                )
6080            }
6081            "merge" => {
6082                let Some(o) = rest.first() else {
6083                    return Ok(false);
6084                };
6085                let o = self.expr_to_string(&o.value)?;
6086                format!(
6087                    "func(__m {map_ty}, __o {map_ty}) {map_ty} {{ \
6088                     __r := make({map_ty}, len(__m)+len(__o)); \
6089                     for __mk, __mv := range __m {{ __r[__mk] = __mv }}; \
6090                     for __ok, __ov := range __o {{ __r[__ok] = __ov }}; \
6091                     return __r }}({recv_str}, {o})"
6092                )
6093            }
6094            "filter" => {
6095                let Some(f) = rest.first() else {
6096                    return Ok(false);
6097                };
6098                let f = self.expr_to_string(&f.value)?;
6099                format!(
6100                    "func(__m {map_ty}, __f func({k_ty}, {v_ty}) bool) {map_ty} {{ \
6101                     __r := make({map_ty}); \
6102                     for __mk, __mv := range __m {{ if __f(__mk, __mv) {{ __r[__mk] = __mv }} }}; \
6103                     return __r }}({recv_str}, {f})"
6104                )
6105            }
6106            "keys" => format!(
6107                "func(__m {map_ty}) []{k_ty} {{ \
6108                 __r := make([]{k_ty}, 0, len(__m)); \
6109                 for __mk := range __m {{ __r = append(__r, __mk) }}; \
6110                 return __r }}({recv_str})"
6111            ),
6112            "values" => format!(
6113                "func(__m {map_ty}) []{v_ty} {{ \
6114                 __r := make([]{v_ty}, 0, len(__m)); \
6115                 for _, __mv := range __m {{ __r = append(__r, __mv) }}; \
6116                 return __r }}({recv_str})"
6117            ),
6118            "entries" | "to_list" => format!(
6119                "func(__m {map_ty}) [][2]interface{{}} {{ \
6120                 __r := make([][2]interface{{}}, 0, len(__m)); \
6121                 for __mk, __mv := range __m {{ __r = append(__r, [2]interface{{}}{{__mk, __mv}}) }}; \
6122                 return __r }}({recv_str})"
6123            ),
6124            "for_each" => {
6125                let Some(f) = rest.first() else {
6126                    return Ok(false);
6127                };
6128                let f = self.expr_to_string(&f.value)?;
6129                format!(
6130                    "func(__m {map_ty}, __f func({k_ty}, {v_ty})) {{ \
6131                     for __mk, __mv := range __m {{ __f(__mk, __mv) }} }}({recv_str}, {f})"
6132                )
6133            }
6134            _ => return Ok(false),
6135        };
6136        self.buf.push_str(&code);
6137        Ok(true)
6138    }
6139
6140    /// Emit a built-in `Set[E]` method call to its Go form (native
6141    /// `map[E]struct{}`, building on P3-α's typed set literals/decls).
6142    ///
6143    /// Recognised via [`crate::generator::desugared_set_method`] (gated on
6144    /// `recv_kind = "Set"`) and wired *before* [`Self::try_emit_list_method`].
6145    /// `contains` is a comma-ok membership test; the set algebra builds new
6146    /// `map[E]struct{}` values. Mutating methods (`add`/`remove`) copy then
6147    /// mutate and return the new set. The inline closures are typed `map[E]
6148    /// struct{}` from the receiver's declared element type
6149    /// ([`Self::set_receiver_elem_go_type`]; `interface{}` fallback). Returns
6150    /// `true` if handled.
6151    fn try_emit_set_method(
6152        &mut self,
6153        node: &AIRNode,
6154        callee: &AIRNode,
6155        args: &[bock_air::AirArg],
6156    ) -> Result<bool, CodegenError> {
6157        let Some((recv, method, rest)) = crate::generator::desugared_set_method(node, callee, args)
6158        else {
6159            return Ok(false);
6160        };
6161        let recv_str = self.expr_to_string(recv)?;
6162        let e_ty = self
6163            .set_receiver_elem_go_type(recv)
6164            .unwrap_or_else(|| "interface{}".to_string());
6165        let set_ty = format!("map[{e_ty}]struct{{}}");
6166        let code = match method {
6167            "len" | "length" | "count" => format!("int64(len({recv_str}))"),
6168            "is_empty" => format!("(len({recv_str}) == 0)"),
6169            "contains" => {
6170                let Some(x) = rest.first() else {
6171                    return Ok(false);
6172                };
6173                let x = self.expr_to_string(&x.value)?;
6174                format!(
6175                    "func(__s {set_ty}, __x {e_ty}) bool {{ _, __ok := __s[__x]; return __ok }}\
6176                     ({recv_str}, {x})"
6177                )
6178            }
6179            "add" => {
6180                let Some(x) = rest.first() else {
6181                    return Ok(false);
6182                };
6183                let x = self.expr_to_string(&x.value)?;
6184                format!(
6185                    "func(__s {set_ty}, __x {e_ty}) {set_ty} {{ \
6186                     __r := make({set_ty}, len(__s)+1); \
6187                     for __sk := range __s {{ __r[__sk] = struct{{}}{{}} }}; \
6188                     __r[__x] = struct{{}}{{}}; return __r }}({recv_str}, {x})"
6189                )
6190            }
6191            "remove" => {
6192                let Some(x) = rest.first() else {
6193                    return Ok(false);
6194                };
6195                let x = self.expr_to_string(&x.value)?;
6196                format!(
6197                    "func(__s {set_ty}, __x {e_ty}) {set_ty} {{ \
6198                     __r := make({set_ty}, len(__s)); \
6199                     for __sk := range __s {{ __r[__sk] = struct{{}}{{}} }}; \
6200                     delete(__r, __x); return __r }}({recv_str}, {x})"
6201                )
6202            }
6203            "union" => {
6204                let Some(o) = rest.first() else {
6205                    return Ok(false);
6206                };
6207                let o = self.expr_to_string(&o.value)?;
6208                format!(
6209                    "func(__a {set_ty}, __b {set_ty}) {set_ty} {{ \
6210                     __r := make({set_ty}, len(__a)+len(__b)); \
6211                     for __k := range __a {{ __r[__k] = struct{{}}{{}} }}; \
6212                     for __k := range __b {{ __r[__k] = struct{{}}{{}} }}; \
6213                     return __r }}({recv_str}, {o})"
6214                )
6215            }
6216            "intersection" => {
6217                let Some(o) = rest.first() else {
6218                    return Ok(false);
6219                };
6220                let o = self.expr_to_string(&o.value)?;
6221                format!(
6222                    "func(__a {set_ty}, __b {set_ty}) {set_ty} {{ \
6223                     __r := make({set_ty}); \
6224                     for __k := range __a {{ if _, __ok := __b[__k]; __ok {{ \
6225                     __r[__k] = struct{{}}{{}} }} }}; \
6226                     return __r }}({recv_str}, {o})"
6227                )
6228            }
6229            "difference" => {
6230                let Some(o) = rest.first() else {
6231                    return Ok(false);
6232                };
6233                let o = self.expr_to_string(&o.value)?;
6234                format!(
6235                    "func(__a {set_ty}, __b {set_ty}) {set_ty} {{ \
6236                     __r := make({set_ty}); \
6237                     for __k := range __a {{ if _, __ok := __b[__k]; !__ok {{ \
6238                     __r[__k] = struct{{}}{{}} }} }}; \
6239                     return __r }}({recv_str}, {o})"
6240                )
6241            }
6242            "is_subset" => {
6243                let Some(o) = rest.first() else {
6244                    return Ok(false);
6245                };
6246                let o = self.expr_to_string(&o.value)?;
6247                format!(
6248                    "func(__a {set_ty}, __b {set_ty}) bool {{ \
6249                     for __k := range __a {{ if _, __ok := __b[__k]; !__ok {{ return false }} }}; \
6250                     return true }}({recv_str}, {o})"
6251                )
6252            }
6253            "is_superset" => {
6254                let Some(o) = rest.first() else {
6255                    return Ok(false);
6256                };
6257                let o = self.expr_to_string(&o.value)?;
6258                format!(
6259                    "func(__a {set_ty}, __b {set_ty}) bool {{ \
6260                     for __k := range __b {{ if _, __ok := __a[__k]; !__ok {{ return false }} }}; \
6261                     return true }}({recv_str}, {o})"
6262                )
6263            }
6264            "filter" => {
6265                let Some(f) = rest.first() else {
6266                    return Ok(false);
6267                };
6268                let f = self.expr_to_string(&f.value)?;
6269                format!(
6270                    "func(__s {set_ty}, __f func({e_ty}) bool) {set_ty} {{ \
6271                     __r := make({set_ty}); \
6272                     for __k := range __s {{ if __f(__k) {{ __r[__k] = struct{{}}{{}} }} }}; \
6273                     return __r }}({recv_str}, {f})"
6274                )
6275            }
6276            "map" => {
6277                let Some(f) = rest.first() else {
6278                    return Ok(false);
6279                };
6280                let f = self.expr_to_string(&f.value)?;
6281                format!(
6282                    "func(__s {set_ty}, __f func({e_ty}) {e_ty}) {set_ty} {{ \
6283                     __r := make({set_ty}); \
6284                     for __k := range __s {{ __r[__f(__k)] = struct{{}}{{}} }}; \
6285                     return __r }}({recv_str}, {f})"
6286                )
6287            }
6288            "to_list" => format!(
6289                "func(__s {set_ty}) []{e_ty} {{ \
6290                 __r := make([]{e_ty}, 0, len(__s)); \
6291                 for __k := range __s {{ __r = append(__r, __k) }}; \
6292                 return __r }}({recv_str})"
6293            ),
6294            "for_each" => {
6295                let Some(f) = rest.first() else {
6296                    return Ok(false);
6297                };
6298                let f = self.expr_to_string(&f.value)?;
6299                format!(
6300                    "func(__s {set_ty}, __f func({e_ty})) {{ \
6301                     for __k := range __s {{ __f(__k) }} }}({recv_str}, {f})"
6302                )
6303            }
6304            _ => return Ok(false),
6305        };
6306        self.buf.push_str(&code);
6307        Ok(true)
6308    }
6309
6310    /// Recognise `Duration.xxx(...)` / `Instant.xxx(...)` associated-function
6311    /// calls and emit inline Go code. Duration values are `int64` nanoseconds
6312    /// (matching `time.Duration`); Instants are `time.Time` (monotonic via
6313    /// `time.Now()`).
6314    fn try_emit_time_assoc_call(
6315        &mut self,
6316        callee: &AIRNode,
6317        args: &[bock_air::AirArg],
6318    ) -> Result<bool, CodegenError> {
6319        let NodeKind::FieldAccess { object, field } = &callee.kind else {
6320            return Ok(false);
6321        };
6322        let NodeKind::Identifier { name: type_name } = &object.kind else {
6323            return Ok(false);
6324        };
6325        let arg_strs: Vec<String> = args
6326            .iter()
6327            .map(|a| self.expr_to_string(&a.value))
6328            .collect::<Result<_, _>>()?;
6329        let arg0 = || arg_strs.first().cloned().unwrap_or_default();
6330        let code = match (type_name.name.as_str(), field.name.as_str()) {
6331            ("Duration", "zero") => "int64(0)".to_string(),
6332            ("Duration", "nanos") => format!("int64({})", arg0()),
6333            ("Duration", "micros") => format!("(int64({}) * 1000)", arg0()),
6334            ("Duration", "millis") => format!("(int64({}) * 1000000)", arg0()),
6335            ("Duration", "seconds") => format!("(int64({}) * 1000000000)", arg0()),
6336            ("Duration", "minutes") => format!("(int64({}) * 60000000000)", arg0()),
6337            ("Duration", "hours") => format!("(int64({}) * 3600000000000)", arg0()),
6338            ("Instant", "now") => {
6339                // Route through an installed `Clock` handler's `now_monotonic`
6340                // op if one is in scope; otherwise emit the host primitive.
6341                if let Some(handler) = self.clock_handler_var() {
6342                    format!("{handler}.{}()", to_pascal_case("now_monotonic"))
6343                } else {
6344                    self.needs_time_import = true;
6345                    "time.Now()".to_string()
6346                }
6347            }
6348            _ => return Ok(false),
6349        };
6350        self.buf.push_str(&code);
6351        Ok(true)
6352    }
6353
6354    /// Lower a primitive trait-bridge method call (`compare`/`eq`/`to_string`/
6355    /// `display` on a primitive receiver) to its Go form.
6356    ///
6357    /// `(1).compare(2)` resolves to `Ordering`; this routes it through the
6358    /// generic Ordering-runtime helper `__bockCompare`, returning a
6359    /// `__bockOrdering` constant the value-switch / construction sides use. `eq`
6360    /// → `==`; `to_string`/`display` → `fmt.Sprintf("%v", x)`.
6361    /// Lower a desugared `String` built-in method call (`recv_kind =
6362    /// "Primitive:String"`) to its native Go string op. Wired into the `Call`
6363    /// arm *before* `try_emit_list_method` so a String receiver's
6364    /// `len`/`contains`/`is_empty` dispatch here, not through the List path —
6365    /// which is the misrouting that broke `String.contains` (the `[]interface{}`
6366    /// `fmt.Sprintf("%v", …)` linear scan failed to compile against a `string`).
6367    ///
6368    /// `len` is the Unicode SCALAR count (`utf8.RuneCountInString(s)`) per spec
6369    /// §18.3 — Go's `len(s)` is the BYTE length, so `byte_len` maps to it.
6370    /// `replace` replaces ALL occurrences (`strings.ReplaceAll`). `split` returns
6371    /// `[]string`, which the read-only `List` built-ins (`len`/…) accept.
6372    ///
6373    /// Gated on `recv_kind = "Primitive:String"` directly (not the cross-backend
6374    /// [`crate::generator::desugared_string_method`] subset) so Go can lower the
6375    /// wider resolved String surface — `slice`/`substring`/`char_at`/`index_of`/
6376    /// `repeat`/`reverse`/`trim_start`/`trim_end` — to native ops, matching the
6377    /// Rust backend. Indexing/`reverse` are **rune-aware** (`[]rune(s)`) so the
6378    /// scalar-index semantics of §18.3 hold for multibyte input — a plain `s[i]`
6379    /// would be a byte. `char_at`/`index_of` build the tagged `Optional` runtime
6380    /// (`__bockSome(v)` / `__bockNone`); `index_of` converts `strings.Index`'s
6381    /// byte offset to a rune index.
6382    fn try_emit_string_method(
6383        &mut self,
6384        node: &AIRNode,
6385        callee: &AIRNode,
6386        args: &[bock_air::AirArg],
6387    ) -> Result<bool, CodegenError> {
6388        if crate::generator::primitive_recv_kind(node) != Some("String") {
6389            return Ok(false);
6390        }
6391        let Some((recv, field, rest)) = crate::generator::desugared_self_call(callee, args) else {
6392            return Ok(false);
6393        };
6394        let method = field.name.as_str();
6395        let recv_str = self.expr_to_string(recv)?;
6396        let arg0 = |this: &mut Self| -> Result<Option<String>, CodegenError> {
6397            rest.first()
6398                .map(|a| this.expr_to_string(&a.value))
6399                .transpose()
6400        };
6401        let code = match method {
6402            "len" | "length" | "count" => {
6403                self.needs_utf8_import = true;
6404                format!("int64(utf8.RuneCountInString({recv_str}))")
6405            }
6406            "byte_len" => format!("int64(len({recv_str}))"),
6407            "is_empty" => format!("(len({recv_str}) == 0)"),
6408            "to_upper" => {
6409                self.needs_strings_import = true;
6410                format!("strings.ToUpper({recv_str})")
6411            }
6412            "to_lower" => {
6413                self.needs_strings_import = true;
6414                format!("strings.ToLower({recv_str})")
6415            }
6416            "trim" => {
6417                self.needs_strings_import = true;
6418                format!("strings.TrimSpace({recv_str})")
6419            }
6420            "trim_start" => {
6421                self.needs_strings_import = true;
6422                self.needs_unicode_import = true;
6423                format!("strings.TrimLeftFunc({recv_str}, unicode.IsSpace)")
6424            }
6425            "trim_end" => {
6426                self.needs_strings_import = true;
6427                self.needs_unicode_import = true;
6428                format!("strings.TrimRightFunc({recv_str}, unicode.IsSpace)")
6429            }
6430            // `reverse` reverses by Unicode scalar (rune), not byte.
6431            "reverse" => format!(
6432                "func(__r []rune) string {{ for __i, __j := 0, len(__r)-1; __i < __j; __i, __j = __i+1, __j-1 {{ __r[__i], __r[__j] = __r[__j], __r[__i] }}; return string(__r) }}([]rune({recv_str}))"
6433            ),
6434            "to_string" | "display" => format!("({recv_str})"),
6435            "repeat" => {
6436                let Some(n) = arg0(self)? else {
6437                    return Ok(false);
6438                };
6439                self.needs_strings_import = true;
6440                format!("strings.Repeat({recv_str}, int({n}))")
6441            }
6442            "contains" => {
6443                let Some(p) = arg0(self)? else {
6444                    return Ok(false);
6445                };
6446                self.needs_strings_import = true;
6447                format!("strings.Contains({recv_str}, {p})")
6448            }
6449            "starts_with" => {
6450                let Some(p) = arg0(self)? else {
6451                    return Ok(false);
6452                };
6453                self.needs_strings_import = true;
6454                format!("strings.HasPrefix({recv_str}, {p})")
6455            }
6456            "ends_with" => {
6457                let Some(p) = arg0(self)? else {
6458                    return Ok(false);
6459                };
6460                self.needs_strings_import = true;
6461                format!("strings.HasSuffix({recv_str}, {p})")
6462            }
6463            "replace" => {
6464                let Some(from) = arg0(self)? else {
6465                    return Ok(false);
6466                };
6467                let Some(to) = rest
6468                    .get(1)
6469                    .map(|a| self.expr_to_string(&a.value))
6470                    .transpose()?
6471                else {
6472                    return Ok(false);
6473                };
6474                self.needs_strings_import = true;
6475                format!("strings.ReplaceAll({recv_str}, {from}, {to})")
6476            }
6477            "split" => {
6478                let Some(sep) = arg0(self)? else {
6479                    return Ok(false);
6480                };
6481                self.needs_strings_import = true;
6482                format!("strings.Split({recv_str}, {sep})")
6483            }
6484            // `slice`/`substring(start, end)`: scalar-index half-open substring
6485            // (spec §18.3). Indexed over `[]rune` so the indices are scalar
6486            // positions; `start`/`end` are clamped into range so out-of-bounds
6487            // does not panic.
6488            "slice" | "substring" => {
6489                let Some(start) = arg0(self)? else {
6490                    return Ok(false);
6491                };
6492                let Some(end) = rest
6493                    .get(1)
6494                    .map(|a| self.expr_to_string(&a.value))
6495                    .transpose()?
6496                else {
6497                    return Ok(false);
6498                };
6499                format!(
6500                    "func(__r []rune, __a, __b int) string {{ if __a < 0 {{ __a = 0 }}; if __b > len(__r) {{ __b = len(__r) }}; if __a > __b {{ __a = __b }}; return string(__r[__a:__b]) }}([]rune({recv_str}), int({start}), int({end}))"
6501                )
6502            }
6503            // `char_at(i)` returns `Optional[Char]` — `None` when out of range. A
6504            // Bock `Char` is a Go `rune`.
6505            "char_at" => {
6506                let Some(i) = arg0(self)? else {
6507                    return Ok(false);
6508                };
6509                format!(
6510                    "func(__r []rune, __i int) __bockOption {{ if __i >= 0 && __i < len(__r) {{ return __bockSome(__r[__i]) }}; return __bockNone }}([]rune({recv_str}), int({i}))"
6511                )
6512            }
6513            // `index_of(needle)` returns `Optional[Int]` — the scalar index of the
6514            // first match, or `None`. `strings.Index` yields a *byte* offset, so
6515            // convert it to a rune index via the prefix rune count.
6516            "index_of" => {
6517                let Some(p) = arg0(self)? else {
6518                    return Ok(false);
6519                };
6520                self.needs_strings_import = true;
6521                self.needs_utf8_import = true;
6522                format!(
6523                    "func(__s, __p string) __bockOption {{ __b := strings.Index(__s, __p); if __b < 0 {{ return __bockNone }}; return __bockSome(int64(utf8.RuneCountInString(__s[:__b]))) }}({recv_str}, {p})"
6524                )
6525            }
6526            _ => return Ok(false),
6527        };
6528        self.buf.push_str(&code);
6529        Ok(true)
6530    }
6531
6532    /// Q-prim-assoc: lower a primitive associated-conversion call
6533    /// (`Float.from(x)` / `Int.try_from(s)` / `String.from(c)`) to Go's native
6534    /// conversion. `from` is a Go type conversion (`float64(x)` / `int64(x)` /
6535    /// `string(x)`; a Bock `Char` is a `rune`, so `string(rune)` yields the
6536    /// single-character string). `try_from` parses via `strconv.Parse{Int,Float}`
6537    /// inside an IIFE returning the `__bockResult` runtime struct, the `Err`
6538    /// payload built with the in-package `ConvertErrorFn` constructor (Go emits
6539    /// everything into `package main`, so it is always visible). Returns `true`
6540    /// when handled.
6541    fn try_emit_primitive_conversion(
6542        &mut self,
6543        node: &AIRNode,
6544        callee: &AIRNode,
6545        args: &[bock_air::AirArg],
6546    ) -> Result<bool, CodegenError> {
6547        let Some((target, method, arg)) =
6548            crate::generator::primitive_conversion_call(node, callee, args)
6549        else {
6550            return Ok(false);
6551        };
6552        let arg_str = self.expr_to_string(arg)?;
6553        let code = match (target, method) {
6554            ("Float", "from") => format!("float64({arg_str})"),
6555            ("Int", "from") => format!("int64({arg_str})"),
6556            ("String", "from") => format!("string({arg_str})"),
6557            ("Int", "try_from") => {
6558                self.needs_strconv_import = true;
6559                self.needs_strings_import = true;
6560                format!(
6561                    "func(__s string) __bockResult {{ __v, __err := strconv.ParseInt(strings.TrimSpace(__s), 10, 64); \
6562                     if __err != nil {{ return __bockErr(ConvertErrorFn(\"cannot parse \\\"\" + __s + \"\\\" as Int\")) }}; \
6563                     return __bockOk(__v) }}({arg_str})"
6564                )
6565            }
6566            ("Float", "try_from") => {
6567                self.needs_strconv_import = true;
6568                self.needs_strings_import = true;
6569                format!(
6570                    "func(__s string) __bockResult {{ __v, __err := strconv.ParseFloat(strings.TrimSpace(__s), 64); \
6571                     if __err != nil {{ return __bockErr(ConvertErrorFn(\"cannot parse \\\"\" + __s + \"\\\" as Float\")) }}; \
6572                     return __bockOk(__v) }}({arg_str})"
6573                )
6574            }
6575            _ => return Ok(false),
6576        };
6577        self.buf.push_str(&code);
6578        Ok(true)
6579    }
6580
6581    /// Lower a desugared numeric/`Char`/`Bool` primitive method (`recv_kind =
6582    /// "Primitive:Int" | "Primitive:Float" | "Primitive:Char" | "Primitive:Bool"`)
6583    /// to its native Go form. Covers the conversion and math methods the checker
6584    /// resolves on the scalar primitives — `to_float`/`to_int`/`abs`/`min`/`max`/
6585    /// `clamp`/`floor`/`ceil`/`round`/`sqrt`/… . Wired into the `Call` arm
6586    /// alongside [`Self::try_emit_string_method`], before the generic
6587    /// desugared-self-call fall-through (which would emit `n.toFloat(n)`).
6588    /// `math.*` operates on `float64`, so the `Float` math methods round-trip the
6589    /// `float64` receiver through `math`. `compare`/`eq`/`to_string`/`display`/
6590    /// `hash_code` stay on the primitive *bridge* path. A Bock `Int` is a Go
6591    /// `int64`, `Float` a `float64`, `Char` a `rune`.
6592    fn try_emit_numeric_method(
6593        &mut self,
6594        node: &AIRNode,
6595        callee: &AIRNode,
6596        args: &[bock_air::AirArg],
6597    ) -> Result<bool, CodegenError> {
6598        let prim = match crate::generator::primitive_recv_kind(node) {
6599            Some(p @ ("Int" | "Float" | "Char" | "Bool")) => p,
6600            _ => return Ok(false),
6601        };
6602        let Some((recv, field, rest)) = crate::generator::desugared_self_call(callee, args) else {
6603            return Ok(false);
6604        };
6605        let method = field.name.as_str();
6606        let recv_str = self.expr_to_string(recv)?;
6607        let arg = |this: &mut Self, i: usize| -> Result<Option<String>, CodegenError> {
6608            rest.get(i)
6609                .map(|a| this.expr_to_string(&a.value))
6610                .transpose()
6611        };
6612        let code = match (prim, method) {
6613            // Conversions. `Float.to_int` truncates toward zero via `math.Trunc`,
6614            // which also yields a *runtime* (non-constant) float64 — Go rejects a
6615            // direct `int64(3.9)` on a literal receiver (an untyped/typed float
6616            // *constant* not exactly representable as an int).
6617            ("Int", "to_float") => format!("float64({recv_str})"),
6618            ("Float", "to_int") => {
6619                self.needs_math_import = true;
6620                format!("int64(math.Trunc({recv_str}))")
6621            }
6622            ("Char", "to_int") => format!("int64({recv_str})"),
6623            ("Bool", "to_int") => {
6624                format!("func(__b bool) int64 {{ if __b {{ return 1 }}; return 0 }}({recv_str})")
6625            }
6626            // Int math. Go 1.21+ has builtin `min`/`max`; `abs` is via a cast to
6627            // float64 and back to keep it inline.
6628            ("Int", "abs") => {
6629                self.needs_math_import = true;
6630                format!("int64(math.Abs(float64({recv_str})))")
6631            }
6632            ("Int" | "Float", "min") => {
6633                let Some(o) = arg(self, 0)? else {
6634                    return Ok(false);
6635                };
6636                format!("min({recv_str}, {o})")
6637            }
6638            ("Int" | "Float", "max") => {
6639                let Some(o) = arg(self, 0)? else {
6640                    return Ok(false);
6641                };
6642                format!("max({recv_str}, {o})")
6643            }
6644            ("Int" | "Float", "clamp") => {
6645                let (Some(lo), Some(hi)) = (arg(self, 0)?, arg(self, 1)?) else {
6646                    return Ok(false);
6647                };
6648                format!("min(max({recv_str}, {lo}), {hi})")
6649            }
6650            ("Int", "shift_left") => {
6651                let Some(o) = arg(self, 0)? else {
6652                    return Ok(false);
6653                };
6654                format!("(({recv_str}) << ({o}))")
6655            }
6656            ("Int", "shift_right") => {
6657                let Some(o) = arg(self, 0)? else {
6658                    return Ok(false);
6659                };
6660                format!("(({recv_str}) >> ({o}))")
6661            }
6662            // Float math (`math.*` works on `float64`).
6663            ("Float", "abs") => {
6664                self.needs_math_import = true;
6665                format!("math.Abs({recv_str})")
6666            }
6667            ("Float", "floor") => {
6668                self.needs_math_import = true;
6669                format!("math.Floor({recv_str})")
6670            }
6671            ("Float", "ceil") => {
6672                self.needs_math_import = true;
6673                format!("math.Ceil({recv_str})")
6674            }
6675            ("Float", "round") => {
6676                self.needs_math_import = true;
6677                format!("math.Round({recv_str})")
6678            }
6679            ("Float", "sqrt") => {
6680                self.needs_math_import = true;
6681                format!("math.Sqrt({recv_str})")
6682            }
6683            ("Float", "is_nan") => {
6684                self.needs_math_import = true;
6685                format!("math.IsNaN({recv_str})")
6686            }
6687            ("Float", "is_infinite") => {
6688                self.needs_math_import = true;
6689                format!("math.IsInf({recv_str}, 0)")
6690            }
6691            // Bool.
6692            ("Bool", "negate") => format!("(!({recv_str}))"),
6693            // Char (a Go `rune`).
6694            ("Char", "to_upper") => {
6695                self.needs_unicode_import = true;
6696                format!("unicode.ToUpper({recv_str})")
6697            }
6698            ("Char", "to_lower") => {
6699                self.needs_unicode_import = true;
6700                format!("unicode.ToLower({recv_str})")
6701            }
6702            ("Char", "is_alpha") => {
6703                self.needs_unicode_import = true;
6704                format!("unicode.IsLetter({recv_str})")
6705            }
6706            ("Char", "is_digit") => {
6707                self.needs_unicode_import = true;
6708                format!("unicode.IsDigit({recv_str})")
6709            }
6710            ("Char", "is_whitespace") => {
6711                self.needs_unicode_import = true;
6712                format!("unicode.IsSpace({recv_str})")
6713            }
6714            _ => return Ok(false),
6715        };
6716        self.buf.push_str(&code);
6717        Ok(true)
6718    }
6719
6720    fn try_emit_primitive_bridge(
6721        &mut self,
6722        node: &AIRNode,
6723        callee: &AIRNode,
6724        args: &[bock_air::AirArg],
6725    ) -> Result<bool, CodegenError> {
6726        let Some((recv, method, rest, prim)) =
6727            crate::generator::primitive_bridge_call(node, callee, args)
6728        else {
6729            return Ok(false);
6730        };
6731        // A concrete primitive receiver uses the typed `__bockCompare` helper.
6732        self.emit_bridge_method(recv, method, rest, false, Some(prim))
6733    }
6734
6735    /// Lower a sealed-core-trait bridge method on a *bounded generic type
6736    /// variable* (`a.eq(b)` / `a.compare(b)` inside `eq_check[T: Equatable]`) to
6737    /// its Go form (GAP-C). The generic analogue of
6738    /// [`Self::try_emit_primitive_bridge`]: the `[T Equatable]` bound is rewritten
6739    /// to Go's built-in constraint (`comparable` / `__bockOrdered`) at the
6740    /// signature, so `==` and the inline ordering comparison type-check. `compare`
6741    /// uses an inline comparison (not the typed `__bockCompare` helper, whose
6742    /// named constraint a `T __bockOrdered` does not satisfy). Fires only when the
6743    /// bound trait is sealed-core and NOT a user-declared trait.
6744    fn try_emit_trait_bound_bridge(
6745        &mut self,
6746        node: &AIRNode,
6747        callee: &AIRNode,
6748        args: &[bock_air::AirArg],
6749    ) -> Result<bool, CodegenError> {
6750        let Some((recv, method, rest, _tr)) =
6751            crate::generator::trait_bound_bridge_call(node, callee, args, &self.trait_decls)
6752        else {
6753            return Ok(false);
6754        };
6755        // A bounded *generic* type var has no concrete primitive kind; the
6756        // Char-display special-case below does not apply.
6757        self.emit_bridge_method(recv, method, rest, true, None)
6758    }
6759
6760    /// Shared body of the primitive / trait-bound bridges. `generic` selects the
6761    /// generic-bound lowering for `compare`: an inline `if a < b … ` expression
6762    /// producing an `__bockOrdering` (the typed `__bockCompare` helper's named
6763    /// constraint is not satisfied by a `T __bockOrdered`-bounded type var). `eq`
6764    /// (`==`) and `to_string`/`display` (`fmt.Sprintf`) are identical either way.
6765    fn emit_bridge_method(
6766        &mut self,
6767        recv: &AIRNode,
6768        method: &str,
6769        rest: &[bock_air::AirArg],
6770        generic: bool,
6771        recv_prim: Option<&str>,
6772    ) -> Result<bool, CodegenError> {
6773        let recv_str = self.expr_to_string(recv)?;
6774        let code = match method {
6775            "compare" => {
6776                let Some(other) = rest.first() else {
6777                    return Ok(false);
6778                };
6779                let other = self.expr_to_string(&other.value)?;
6780                if generic {
6781                    // The Ordering runtime (gated on `"compare"` appearing in the
6782                    // module AST) is already emitted at module top, so `Less`/
6783                    // `Equal`/`Greater`/`__bockOrdering` are in scope here.
6784                    format!(
6785                        "func() __bockOrdering {{ if ({recv_str}) < ({other}) {{ return Less }}; \
6786                         if ({recv_str}) == ({other}) {{ return Equal }}; return Greater }}()"
6787                    )
6788                } else {
6789                    format!("__bockCompare({recv_str}, {other})")
6790                }
6791            }
6792            "eq" => {
6793                let Some(other) = rest.first() else {
6794                    return Ok(false);
6795                };
6796                let other = self.expr_to_string(&other.value)?;
6797                format!("(({recv_str}) == ({other}))")
6798            }
6799            "to_string" | "display" => {
6800                // A `Char` lowers to Go `rune` (an alias of `int32`); `fmt.Sprintf
6801                // ("%v", r)` would print its integer code point ('A' → "65"), not
6802                // the character. `string(rune)` renders the scalar as its UTF-8
6803                // text. Every other primitive keeps the `%v` formatting (an int
6804                // prints as its digits, a float/bool/string as itself).
6805                if recv_prim == Some("Char") {
6806                    format!("string({recv_str})")
6807                } else {
6808                    self.needs_fmt_import = true;
6809                    format!("fmt.Sprintf(\"%v\", {recv_str})")
6810                }
6811            }
6812            _ => return Ok(false),
6813        };
6814        self.buf.push_str(&code);
6815        Ok(true)
6816    }
6817
6818    /// Recognise desugared method calls on Duration/Instant values.
6819    ///
6820    /// `node` is the full `Call` AIR node, consulted only to *exclude* primitive
6821    /// receivers: [`is_time_method_name`] alone is ambiguous (`abs` is both
6822    /// `Duration.abs` and `Int.abs`/`Float.abs`), so when the checker has stamped
6823    /// `recv_kind = "Primitive:<Ty>"` on the call this is a numeric method, not a
6824    /// time method — bail so [`Self::try_emit_numeric_method`] handles it.
6825    fn try_emit_time_desugared_method(
6826        &mut self,
6827        node: &AIRNode,
6828        callee: &AIRNode,
6829        args: &[bock_air::AirArg],
6830    ) -> Result<bool, CodegenError> {
6831        if crate::generator::primitive_recv_kind(node).is_some() {
6832            return Ok(false);
6833        }
6834        let NodeKind::FieldAccess { object, field } = &callee.kind else {
6835            return Ok(false);
6836        };
6837        if let NodeKind::Identifier { name } = &object.kind {
6838            if matches!(name.name.as_str(), "Duration" | "Instant") {
6839                return Ok(false);
6840            }
6841        }
6842        if !is_time_method_name(&field.name) {
6843            return Ok(false);
6844        }
6845        let remaining: Vec<bock_air::AirArg> = args.iter().skip(1).cloned().collect();
6846        self.try_emit_time_method(object, &field.name, &remaining)
6847    }
6848
6849    /// Recognise `Channel.new()`, `spawn(...)`, and method calls on a
6850    /// channel value. Emits calls into the Go runtime helper code
6851    /// (injected at top-of-module).
6852    fn try_emit_concurrency_call(
6853        &mut self,
6854        callee: &AIRNode,
6855        args: &[bock_air::AirArg],
6856    ) -> Result<bool, CodegenError> {
6857        if let NodeKind::Identifier { name } = &callee.kind {
6858            if name.name == "spawn" {
6859                self.buf.push_str("__bockSpawn(");
6860                for (i, arg) in args.iter().enumerate() {
6861                    if i > 0 {
6862                        self.buf.push_str(", ");
6863                    }
6864                    self.emit_expr(&arg.value)?;
6865                }
6866                self.buf.push(')');
6867                return Ok(true);
6868            }
6869        }
6870        let NodeKind::FieldAccess { object, field } = &callee.kind else {
6871            return Ok(false);
6872        };
6873        if let NodeKind::Identifier { name: type_name } = &object.kind {
6874            if type_name.name == "Channel" && field.name == "new" {
6875                self.buf.push_str("__bockChannelNew()");
6876                return Ok(true);
6877            }
6878        }
6879        if matches!(field.name.as_str(), "send" | "recv" | "close") {
6880            self.emit_expr(object)?;
6881            let _ = write!(self.buf, ".{}", field.name);
6882            self.buf.push('(');
6883            for (i, arg) in args.iter().skip(1).enumerate() {
6884                if i > 0 {
6885                    self.buf.push_str(", ");
6886                }
6887                self.emit_expr(&arg.value)?;
6888            }
6889            self.buf.push(')');
6890            return Ok(true);
6891        }
6892        Ok(false)
6893    }
6894
6895    /// Recognise instance methods on Duration/Instant values.
6896    fn try_emit_time_method(
6897        &mut self,
6898        receiver: &AIRNode,
6899        method: &str,
6900        args: &[bock_air::AirArg],
6901    ) -> Result<bool, CodegenError> {
6902        let recv_str = self.expr_to_string(receiver)?;
6903        let arg_strs: Vec<String> = args
6904            .iter()
6905            .map(|a| self.expr_to_string(&a.value))
6906            .collect::<Result<_, _>>()?;
6907        let code = match method {
6908            "as_nanos" => format!("({recv_str})"),
6909            "as_millis" => format!("(({recv_str}) / 1000000)"),
6910            "as_seconds" => format!("(({recv_str}) / 1000000000)"),
6911            "is_zero" => format!("(({recv_str}) == 0)"),
6912            "is_negative" => format!("(({recv_str}) < 0)"),
6913            "abs" => {
6914                format!("(func(__d int64) int64 {{ if __d < 0 {{ return -__d }}; return __d }}({recv_str}))")
6915            }
6916            "elapsed" => {
6917                // `instant.elapsed()` is derived: time-since-`recv`. Route the
6918                // "now" read through an installed `Clock` handler if in scope —
6919                // `NowMonotonic()` yields a `time.Time`, so the span is
6920                // `now.Sub(recv)` as nanoseconds; otherwise read the host
6921                // monotonic clock via `time.Since(recv)` (default).
6922                if let Some(handler) = self.clock_handler_var() {
6923                    format!(
6924                        "int64({handler}.{}().Sub({recv_str}))",
6925                        to_pascal_case("now_monotonic")
6926                    )
6927                } else {
6928                    self.needs_time_import = true;
6929                    format!("int64(time.Since({recv_str}))")
6930                }
6931            }
6932            "duration_since" => {
6933                let other = arg_strs.first().cloned().unwrap_or_default();
6934                format!("int64(({recv_str}).Sub({other}))")
6935            }
6936            _ => return Ok(false),
6937        };
6938        self.buf.push_str(&code);
6939        Ok(true)
6940    }
6941
6942    // ── Top-level dispatch ──────────────────────────────────────────────────
6943
6944    fn emit_node(&mut self, node: &AIRNode) -> Result<(), CodegenError> {
6945        match &node.kind {
6946            NodeKind::Module { items, .. } => {
6947                if self.per_module {
6948                    // Per-module native-package path (S3): each module is its own
6949                    // `package main` file and the runtime preludes live once in
6950                    // the shared `bock_runtime.go` (same package → visible). So
6951                    // this file inlines NO prelude; it just emits its own items.
6952                    // `ImportDecl`s are a no-op (same package — no inter-file
6953                    // import). The per-file `import (...)` block (fmt/sync/…) is
6954                    // rendered by `into_parts` from the `needs_*` flags the body
6955                    // sets as it emits.
6956                    //
6957                    // `@test` functions are transpiled separately into `go test`
6958                    // files (project mode, §20.6.2 — see `generate_tests`), never
6959                    // into the runtime package: their `expect(...)` assertion DSL
6960                    // has no runtime definition in the emitted source.
6961                    let mut first = true;
6962                    for item in items.iter() {
6963                        if crate::generator::fn_is_test(item) {
6964                            continue;
6965                        }
6966                        if !first {
6967                            self.buf.push('\n');
6968                        }
6969                        first = false;
6970                        self.emit_node(item)?;
6971                    }
6972                    return Ok(());
6973                }
6974                // Single-module self-contained emit (`generate_module`, used by
6975                // unit tests): the module's runtime preludes are inlined into
6976                // this one file's buffer and `ImportDecl`s are dropped. Each
6977                // prelude is emitted at most once, gated on a ctx flag (a
6978                // duplicate `type __bockChannel`/`__bockOption` would not
6979                // compile). The per-module *project* path never takes this branch
6980                // (it sets `per_module` and emits preludes once into the shared
6981                // `bock_runtime.go`).
6982                if !self.concurrency_runtime_emitted && go_module_uses_concurrency(items) {
6983                    self.buf.push_str(CONCURRENCY_RUNTIME_GO);
6984                    self.buf.push('\n');
6985                    self.concurrency_runtime_emitted = true;
6986                }
6987                let uses_optional = go_module_uses_optional(items);
6988                let uses_result = go_module_uses_result(items);
6989                // The deep-eq runtime's `__bockEqCustom` type-asserts the
6990                // Optional/Result wrapper structs to defer their payload to the
6991                // element's `Eq` (Q-py-go-wrapper-structural-eq), so the wrapper
6992                // type definitions must be in scope wherever `__bockEqCustom` is.
6993                // Emit them when the module uses a deep-eq lane even if it never
6994                // names `Optional`/`Result`; the (then-unused) constructors are
6995                // harmless package-level decls in Go.
6996                let uses_deep_eq = go_module_uses_deep_eq(items);
6997                if !self.optional_runtime_emitted && (uses_optional || uses_deep_eq) {
6998                    self.buf.push_str(OPTIONAL_RUNTIME_GO);
6999                    self.buf.push('\n');
7000                    self.optional_runtime_emitted = true;
7001                }
7002                if !self.result_runtime_emitted && (uses_result || uses_deep_eq) {
7003                    self.buf.push_str(RESULT_RUNTIME_GO);
7004                    self.buf.push('\n');
7005                    self.result_runtime_emitted = true;
7006                }
7007                // Shared numeric-payload helpers: emit once if either container
7008                // runtime is present (both use them; emitting from each would
7009                // redeclare them).
7010                if !self.numeric_runtime_emitted && (uses_optional || uses_result) {
7011                    self.buf.push_str(NUMERIC_RUNTIME_GO);
7012                    self.buf.push('\n');
7013                    self.numeric_runtime_emitted = true;
7014                }
7015                // The bespoke `__bockOrdering` value runtime is emitted only when
7016                // the real `core.compare.Ordering` enum is NOT reachable — when
7017                // it is, that user enum is authoritative (its variants are the
7018                // sealed-interface structs `OrderingLess{}`, and `compare`
7019                // returns it), so the int runtime would be dead and its `Less`
7020                // constants would shadow nothing the program uses.
7021                let emit_ordering = !self.ordering_runtime_emitted
7022                    && go_module_uses_ordering(items)
7023                    && !self.ordering_enum_reachable();
7024                if emit_ordering {
7025                    self.buf.push_str(ORDERING_RUNTIME_GO);
7026                    self.buf.push('\n');
7027                    self.ordering_runtime_emitted = true;
7028                }
7029                // The `__bockOrdered` constraint: needed by a `[T: Comparable]`
7030                // sealed-bound generic fn, or whenever the Ordering runtime above
7031                // is emitted (the constraint was split out of that block). Deduped
7032                // with its own flag so it is defined at most once.
7033                if !self.ordered_constraint_emitted
7034                    && (emit_ordering || !self.fn_sealed_bound.is_empty())
7035                {
7036                    self.buf.push_str(ORDERED_CONSTRAINT_GO);
7037                    self.buf.push('\n');
7038                    self.ordered_constraint_emitted = true;
7039                }
7040                if !self.range_runtime_emitted && go_module_uses_range(items) {
7041                    self.buf.push_str(RANGE_RUNTIME_GO);
7042                    self.buf.push('\n');
7043                    self.range_runtime_emitted = true;
7044                }
7045                if !self.int_pow_runtime_emitted && go_module_uses_int_pow(items) {
7046                    self.buf.push_str(INT_POW_RUNTIME_GO);
7047                    self.buf.push('\n');
7048                    self.int_pow_runtime_emitted = true;
7049                }
7050                if !self.deep_eq_runtime_emitted && uses_deep_eq {
7051                    self.buf.push_str(DEEP_EQ_RUNTIME_GO);
7052                    self.buf.push('\n');
7053                    self.deep_eq_runtime_emitted = true;
7054                    self.needs_reflect_import = true;
7055                }
7056                // `@test` functions are transpiled separately into `go test` files
7057                // (project mode, §20.6.2 — see `generate_tests`), never into the
7058                // runtime package.
7059                let mut first = true;
7060                for item in items.iter() {
7061                    if crate::generator::fn_is_test(item) {
7062                        continue;
7063                    }
7064                    if !first {
7065                        self.buf.push('\n');
7066                    }
7067                    first = false;
7068                    self.emit_node(item)?;
7069                }
7070                Ok(())
7071            }
7072            NodeKind::ImportDecl { .. } => {
7073                // Single-module self-contained emit: a Bock `use` is a no-op (no
7074                // sibling file to import from). The per-module project path keeps
7075                // one `package main` across files, so a same-package symbol is
7076                // also visible without a Go `import` — the per-item visit here is
7077                // a no-op in both paths.
7078                Ok(())
7079            }
7080            NodeKind::FnDecl {
7081                visibility,
7082                is_async,
7083                name,
7084                generic_params,
7085                params,
7086                return_type,
7087                effect_clause,
7088                where_clause,
7089                body,
7090                ..
7091            } => {
7092                // Fold any `where`-clause trait bounds onto the generic params so
7093                // the `[T Constraint]` type-param constraint is emitted for a
7094                // `where`-bounded fn — local or imported (the imported fn is
7095                // emitted in its own module file with its reconstructed
7096                // `where`-clause, PR #286).
7097                let merged = crate::generator::merge_where_bounds_into_generics(
7098                    generic_params,
7099                    where_clause,
7100                );
7101                self.emit_fn_decl(
7102                    *visibility,
7103                    *is_async,
7104                    &name.name,
7105                    &merged,
7106                    params,
7107                    return_type.as_deref(),
7108                    effect_clause,
7109                    body,
7110                )
7111            }
7112            NodeKind::RecordDecl {
7113                name,
7114                generic_params,
7115                fields,
7116                ..
7117            } => {
7118                let type_params = self.format_generic_params(generic_params);
7119                self.writeln(&format!("type {}{type_params} struct {{", name.name));
7120                self.indent += 1;
7121                for f in fields {
7122                    let type_str = self.ast_type_to_go(&f.ty);
7123                    self.writeln(&format!("{}\t{type_str}", to_pascal_case(&f.name.name)));
7124                }
7125                self.indent -= 1;
7126                self.writeln("}");
7127                Ok(())
7128            }
7129            NodeKind::EnumDecl {
7130                name,
7131                generic_params,
7132                variants,
7133                ..
7134            } => {
7135                // Go doesn't have algebraic types; use interface + variant structs.
7136                let type_params = self.format_generic_params(generic_params);
7137                // Emit the interface (sealed by convention).
7138                self.writeln(&format!("type {}{type_params} interface {{", name.name));
7139                self.indent += 1;
7140                self.writeln(&format!("is{}()", name.name));
7141                self.indent -= 1;
7142                self.writeln("}");
7143                // Emit each variant as a struct implementing the interface.
7144                for variant in variants {
7145                    self.buf.push('\n');
7146                    self.emit_enum_variant(&name.name, generic_params, variant)?;
7147                }
7148                Ok(())
7149            }
7150            NodeKind::ClassDecl {
7151                name,
7152                generic_params,
7153                fields,
7154                methods,
7155                ..
7156            } => {
7157                // Emit struct.
7158                let type_params = self.format_generic_params(generic_params);
7159                self.writeln(&format!("type {}{type_params} struct {{", name.name));
7160                self.indent += 1;
7161                for f in fields {
7162                    let type_str = self.ast_type_to_go(&f.ty);
7163                    self.writeln(&format!("{}\t{type_str}", to_pascal_case(&f.name.name)));
7164                }
7165                self.indent -= 1;
7166                self.writeln("}");
7167                // Constructor function.
7168                if !fields.is_empty() {
7169                    self.buf.push('\n');
7170                    let params: Vec<String> = fields
7171                        .iter()
7172                        .map(|f| {
7173                            let fname = to_camel_case(&f.name.name);
7174                            let type_str = self.ast_type_to_go(&f.ty);
7175                            format!("{fname} {type_str}")
7176                        })
7177                        .collect();
7178                    self.writeln(&format!(
7179                        "func New{}({}) *{} {{",
7180                        name.name,
7181                        params.join(", "),
7182                        name.name
7183                    ));
7184                    self.indent += 1;
7185                    let field_inits: Vec<String> = fields
7186                        .iter()
7187                        .map(|f| {
7188                            format!(
7189                                "{}: {},",
7190                                to_pascal_case(&f.name.name),
7191                                to_camel_case(&f.name.name)
7192                            )
7193                        })
7194                        .collect();
7195                    self.writeln(&format!("return &{} {{", name.name));
7196                    self.indent += 1;
7197                    for init in &field_inits {
7198                        self.writeln(init);
7199                    }
7200                    self.indent -= 1;
7201                    self.writeln("}");
7202                    self.indent -= 1;
7203                    self.writeln("}");
7204                }
7205                // Methods.
7206                for method in methods {
7207                    self.buf.push('\n');
7208                    self.emit_method(&name.name, generic_params, method, false)?;
7209                }
7210                Ok(())
7211            }
7212            NodeKind::TraitDecl {
7213                name,
7214                methods,
7215                generic_params,
7216                ..
7217            } => {
7218                // Traits → Go interfaces. A trait whose methods take a
7219                // `Self`-typed operand is encoded as an F-bounded *generic*
7220                // interface — `type Comparable[__Self any] interface {
7221                // Compare(__Self) Ordering }` — so an impl `func (Key)
7222                // Compare(Key)` satisfies `Comparable[Key]` and a bound `[T:
7223                // Comparable]` lowers to `[T Comparable[T]]`. `Self` in the
7224                // method signatures then renders as the interface's type param.
7225                //
7226                // A trait that declares its own generic params
7227                // (`trait Iterable[T] { fn iter(self) -> ListIterator[T] }`)
7228                // must carry them on the interface header too, or the param
7229                // appears `undefined` in a method signature (`Iter()
7230                // ListIterator[T]` with no `[T any]` → Go `undefined: T`). The
7231                // declared params and the synthesized `__Self` (if any) are both
7232                // threaded into the header.
7233                let uses_self = self.self_param_traits.contains(&name.name);
7234                let prev_self_subst = self.go_self_subst.take();
7235                let mut header_params: Vec<String> = Vec::new();
7236                if uses_self {
7237                    self.go_self_subst = Some("__Self".to_string());
7238                    header_params.push("__Self any".to_string());
7239                }
7240                for p in generic_params {
7241                    header_params.push(format!("{} any", p.name.name));
7242                }
7243                let head = if header_params.is_empty() {
7244                    name.name.clone()
7245                } else {
7246                    format!("{}[{}]", name.name, header_params.join(", "))
7247                };
7248                self.writeln(&format!("type {head} interface {{"));
7249                self.indent += 1;
7250                for method in methods {
7251                    if let NodeKind::FnDecl {
7252                        name,
7253                        params,
7254                        return_type,
7255                        ..
7256                    } = &method.kind
7257                    {
7258                        // Drop the leading `self` receiver — a Go interface
7259                        // method's receiver is implicit, so only the remaining
7260                        // operands form the signature (the AIR keeps `self` as a
7261                        // real leading param, as for impl methods).
7262                        let rest = match params.first().map(crate::generator::param_binds_self) {
7263                            Some(Some(_)) => &params[1..],
7264                            _ => &params[..],
7265                        };
7266                        let param_strs = self.collect_param_type_strs(rest);
7267                        let is_void = return_type.as_deref().is_some_and(Self::is_void_type);
7268                        let ret = if is_void {
7269                            String::new()
7270                        } else {
7271                            return_type
7272                                .as_deref()
7273                                .map(|t| format!(" {}", self.type_to_go(t)))
7274                                .unwrap_or_default()
7275                        };
7276                        self.writeln(&format!(
7277                            "{}({}){ret}",
7278                            self.go_method_name(&name.name, true),
7279                            param_strs.join(", "),
7280                        ));
7281                    }
7282                }
7283                self.indent -= 1;
7284                self.writeln("}");
7285                self.go_self_subst = prev_self_subst;
7286                Ok(())
7287            }
7288            NodeKind::ImplBlock {
7289                generic_params,
7290                target,
7291                methods,
7292                trait_path,
7293                ..
7294            } => {
7295                let target_name = self.type_expr_to_string(target);
7296                // The receiver's type-param list. Go requires the parameters on a
7297                // generic type's method receiver: `func (self *Box[T]) ...`. The
7298                // params come from the impl's own list when present, else from the
7299                // record/enum decl (the common `impl Box { ... }` where `T` is
7300                // declared on `record Box[T]`, not the impl).
7301                let target_generics = self.impl_target_generics(generic_params, &target_name);
7302                // Value receivers for trait/effect impls so `Handler{}` satisfies
7303                // the interface; pointer receivers for inherent `impl T { ... }`.
7304                let use_value_receiver = trait_path.is_some();
7305                // Trait default methods (codegen-completeness P2): synthesize a
7306                // receiver method on the target for every default method this
7307                // impl does not override, so the type satisfies the interface
7308                // (which declares the default's signature) and a call resolves.
7309                // A default body calling another trait method (`self.other(..)`)
7310                // resolves through the same receiver methods.
7311                let default_methods: Vec<AIRNode> = trait_path
7312                    .as_ref()
7313                    .map(|tp| {
7314                        crate::generator::inherited_default_methods(&self.trait_decls, tp, methods)
7315                    })
7316                    .unwrap_or_default();
7317                let mut emitted_any = false;
7318                for method in methods.iter().chain(default_methods.iter()) {
7319                    // Skip a trait-impl method that duplicates an inherent
7320                    // (`impl Type`) / class method of the same Go name. The
7321                    // inherent method is the real implementation and — being in
7322                    // `public_methods` because the trait declares it — is now
7323                    // exported to the same PascalCase Go name, so it satisfies the
7324                    // interface directly. Emitting the trait-impl method too would
7325                    // be a duplicate-method Go error, and a forwarder body
7326                    // (`fn render(self) { self.render() }`) would resolve back to
7327                    // itself (`Render() { return self.Render() }`) — infinite
7328                    // recursion. Default methods (synthesized, real bodies) and
7329                    // non-duplicated trait methods are unaffected.
7330                    if trait_path.is_some() {
7331                        if let NodeKind::FnDecl { name, .. } = &method.kind {
7332                            let go_name = to_pascal_case(&name.name);
7333                            if self
7334                                .inherent_methods
7335                                .contains(&(target_name.clone(), go_name))
7336                            {
7337                                continue;
7338                            }
7339                        }
7340                    }
7341                    if emitted_any {
7342                        self.buf.push('\n');
7343                    }
7344                    self.emit_method(&target_name, &target_generics, method, use_value_receiver)?;
7345                    emitted_any = true;
7346                }
7347                Ok(())
7348            }
7349            NodeKind::EffectDecl {
7350                name,
7351                components,
7352                generic_params,
7353                operations,
7354                ..
7355            } => {
7356                if !components.is_empty() {
7357                    let comp_names: Vec<String> = components
7358                        .iter()
7359                        .map(|tp| {
7360                            tp.segments
7361                                .last()
7362                                .map_or("effect".to_string(), |s| s.name.clone())
7363                        })
7364                        .collect();
7365                    self.writeln(&format!(
7366                        "// composite effect {} = {}",
7367                        name.name,
7368                        comp_names.join(" + ")
7369                    ));
7370                    self.composite_effects.insert(name.name.clone(), comp_names);
7371                    return Ok(());
7372                }
7373                // Record effect operations for Call → handler.op rewriting.
7374                for op in operations {
7375                    if let NodeKind::FnDecl {
7376                        name: op_name,
7377                        return_type,
7378                        ..
7379                    } = &op.kind
7380                    {
7381                        self.effect_ops
7382                            .insert(op_name.name.clone(), name.name.clone());
7383                        if return_type.as_deref().is_some_and(Self::is_void_type) {
7384                            self.void_effect_ops.insert(op_name.name.clone());
7385                        }
7386                    }
7387                }
7388                // Effects → Go interfaces.
7389                let type_params = self.format_generic_params(generic_params);
7390                self.writeln(&format!("type {}{type_params} interface {{", name.name));
7391                self.indent += 1;
7392                for op in operations {
7393                    if let NodeKind::FnDecl {
7394                        name,
7395                        params,
7396                        return_type,
7397                        ..
7398                    } = &op.kind
7399                    {
7400                        let param_strs = self.collect_param_type_strs(params);
7401                        let is_void = return_type.as_deref().is_some_and(Self::is_void_type);
7402                        let ret = if is_void {
7403                            String::new()
7404                        } else {
7405                            return_type
7406                                .as_deref()
7407                                .map(|t| format!(" {}", self.type_to_go(t)))
7408                                .unwrap_or_default()
7409                        };
7410                        self.writeln(&format!(
7411                            "{}({}){ret}",
7412                            to_pascal_case(&name.name),
7413                            param_strs.join(", "),
7414                        ));
7415                    }
7416                }
7417                self.indent -= 1;
7418                self.writeln("}");
7419                Ok(())
7420            }
7421            NodeKind::TypeAlias {
7422                name,
7423                generic_params,
7424                ty,
7425                ..
7426            } => {
7427                // Render the alias to its underlying Go type so a value of the
7428                // alias type is the concrete container/struct (`type ParseResult =
7429                // Result[...]` → `type ParseResult = __bockResult`, whose `.tag`
7430                // a `match` reads). A *generic* alias keeps the prior `interface{}`
7431                // erasure (the emitter does not substitute its params, and the
7432                // alias registry skips it), so its param list is preserved.
7433                if generic_params.is_empty() {
7434                    let underlying = self.type_to_go(ty);
7435                    self.writeln(&format!("type {} = {underlying}", name.name));
7436                } else {
7437                    let type_params = self.format_generic_params(generic_params);
7438                    self.writeln(&format!("type {}{type_params} = interface{{}}", name.name));
7439                }
7440                Ok(())
7441            }
7442            NodeKind::ConstDecl {
7443                name, value, ty, ..
7444            } => {
7445                let type_str = format!(" {}", self.type_to_go(ty));
7446                let ind = self.indent_str();
7447                // Emit the const's declared name verbatim (not pascal-cased) so it
7448                // matches the verbatim spelling the `Identifier` use-site arm emits
7449                // for a known const — `to_pascal_case` strips underscores
7450                // (`FIZZ_NUM` → `FIZZNUM`) while the use site keeps `FIZZ_NUM`, an
7451                // undefined-identifier error. `SCREAMING_SNAKE` is a valid exported
7452                // Go identifier.
7453                let _ = write!(self.buf, "{ind}var {}{type_str} = ", name.name);
7454                self.emit_expr(value)?;
7455                self.buf.push('\n');
7456                Ok(())
7457            }
7458            NodeKind::ModuleHandle { effect, handler } => {
7459                let effect_name = effect.segments.last().map_or("effect", |s| s.name.as_str());
7460                let var_name = format!("__{}", to_camel_case(effect_name));
7461                let ind = self.indent_str();
7462                let _ = write!(self.buf, "{ind}var {var_name} {effect_name} = ");
7463                self.emit_expr(handler)?;
7464                self.buf.push('\n');
7465                // Register the module-scoped handler so effectful function
7466                // calls at module level pick it up.
7467                self.current_handler_vars
7468                    .insert(effect_name.to_string(), var_name);
7469                Ok(())
7470            }
7471            NodeKind::PropertyTest { name, .. } => {
7472                self.writeln(&format!("// property test: {name}"));
7473                Ok(())
7474            }
7475            // Statement / expression nodes at top level:
7476            NodeKind::LetBinding { .. }
7477            | NodeKind::If { .. }
7478            | NodeKind::For { .. }
7479            | NodeKind::While { .. }
7480            | NodeKind::Loop { .. }
7481            | NodeKind::Return { .. }
7482            | NodeKind::Break { .. }
7483            | NodeKind::Continue
7484            | NodeKind::Guard { .. }
7485            | NodeKind::Match { .. }
7486            | NodeKind::Block { .. }
7487            | NodeKind::HandlingBlock { .. }
7488            | NodeKind::Assign { .. } => self.emit_stmt(node),
7489            // A bare `expr?` statement (`save_task(task)?`): the success value is
7490            // discarded, but the failure path must still early-return the
7491            // propagated error/None from the enclosing function. Emit the unwrap
7492            // prelude only — the success payload is unused, and asserting it (e.g.
7493            // a `Result[Void, _]` whose `Ok(())` boxes `nil`) would panic.
7494            NodeKind::Propagate { expr: inner } => {
7495                let _ = self.emit_try_unwrap(inner)?;
7496                Ok(())
7497            }
7498            _ => {
7499                // DQ18: an in-place `List` mutator (`push`/`append`) in
7500                // statement position lowers to Go's slice-growth idiom
7501                // `recv = append(recv, x)` — an assignment statement, not the
7502                // value-less call the other backends emit. Intercept it here,
7503                // before the generic expression-statement fall-through (Go has no
7504                // expression form for in-place append).
7505                if let NodeKind::Call { callee, args, .. } = &node.kind {
7506                    if self.try_emit_list_mutating_stmt(node, callee, args)? {
7507                        return Ok(());
7508                    }
7509                }
7510                self.write_indent();
7511                self.emit_expr(node)?;
7512                self.buf.push('\n');
7513                Ok(())
7514            }
7515        }
7516    }
7517
7518    // ── Generics ────────────────────────────────────────────────────────────
7519
7520    /// Resolve the generic params that apply to an `impl` target. Prefers the
7521    /// impl's own params (`impl[T] Box[T] { ... }`); falls back to the generic
7522    /// params declared on the target record/enum (`impl Box { ... }` where `T`
7523    /// is declared on `record Box[T]`). Returns an empty slice for a
7524    /// non-generic target.
7525    fn impl_target_generics(
7526        &self,
7527        impl_params: &[bock_ast::GenericParam],
7528        target_name: &str,
7529    ) -> Vec<bock_ast::GenericParam> {
7530        if !impl_params.is_empty() {
7531            return impl_params.to_vec();
7532        }
7533        self.generic_decls
7534            .get(target_name)
7535            .cloned()
7536            .unwrap_or_default()
7537    }
7538
7539    /// Render a *use-site* generic argument list (`[T]`) from generic params —
7540    /// the bare names only, never the `any`/bound clause. Used on a method
7541    /// receiver type (`*Box[T]`) where the params are already in scope.
7542    fn format_generic_param_args(&self, params: &[bock_ast::GenericParam]) -> String {
7543        if params.is_empty() {
7544            return String::new();
7545        }
7546        let names: Vec<&str> = params.iter().map(|p| p.name.name.as_str()).collect();
7547        format!("[{}]", names.join(", "))
7548    }
7549
7550    fn format_generic_params(&self, params: &[bock_ast::GenericParam]) -> String {
7551        if params.is_empty() {
7552            return String::new();
7553        }
7554        let parts: Vec<String> = params
7555            .iter()
7556            .map(|p| {
7557                if p.bounds.is_empty() {
7558                    format!("{} any", p.name.name)
7559                } else {
7560                    let bound_strs: Vec<String> = p
7561                        .bounds
7562                        .iter()
7563                        .map(|b| {
7564                            let bound_name = b
7565                                .segments
7566                                .iter()
7567                                .map(|s| s.name.as_str())
7568                                .collect::<Vec<_>>()
7569                                .join(".");
7570                            // A compiler-provided sealed-core bound (`Equatable`/…)
7571                            // with no user `impl` maps to Go's built-in constraint
7572                            // (GAP-C): `comparable` for equality/hashing, the
7573                            // self-contained `__bockOrdered` set for ordering, `any`
7574                            // for stringable. There is no `Equatable` type in Go, so
7575                            // the verbatim bound would be `undefined`.
7576                            if crate::generator::is_unimplemented_sealed_core_trait(
7577                                &bound_name,
7578                                &self.trait_decls,
7579                            ) {
7580                                match bound_name.as_str() {
7581                                    "Equatable" | "Hashable" => "comparable".to_string(),
7582                                    "Comparable" => "__bockOrdered".to_string(),
7583                                    _ => "any".to_string(),
7584                                }
7585                            } else if self.self_param_traits.contains(&bound_name) {
7586                                // An F-bounded self-param trait constraint is applied
7587                                // to the type var itself: `[T Comparable[T]]`.
7588                                format!("{bound_name}[{}]", p.name.name)
7589                            } else {
7590                                bound_name
7591                            }
7592                        })
7593                        .collect();
7594                    format!("{} {}", p.name.name, bound_strs.join(" | "))
7595                }
7596            })
7597            .collect();
7598        format!("[{}]", parts.join(", "))
7599    }
7600
7601    fn format_generic_args(&self, args: &[AIRNode]) -> String {
7602        if args.is_empty() {
7603            return String::new();
7604        }
7605        let parts: Vec<String> = args.iter().map(|a| self.type_to_go(a)).collect();
7606        format!("[{}]", parts.join(", "))
7607    }
7608
7609    // ── Function declarations ───────────────────────────────────────────────
7610
7611    #[allow(clippy::too_many_arguments)]
7612    fn emit_fn_decl(
7613        &mut self,
7614        visibility: Visibility,
7615        is_async: bool,
7616        name: &str,
7617        generic_params: &[bock_ast::GenericParam],
7618        params: &[AIRNode],
7619        return_type: Option<&AIRNode>,
7620        effect_clause: &[bock_ast::TypePath],
7621        body: &AIRNode,
7622    ) -> Result<(), CodegenError> {
7623        let is_public = matches!(visibility, Visibility::Public);
7624        // The program entry point is always Go's `func main()`, never the
7625        // exported `func Main()` that PascalCasing a `public fn main` would
7626        // produce (Go would then report "function main is undeclared").
7627        let is_entry_point = name == "main";
7628        if is_public && !is_entry_point {
7629            self.public_fns.insert(name.to_string());
7630        }
7631        // `main` stays Go's bare `func main`; every other function goes through
7632        // `go_fn_name`, which applies the public/private casing rule and renames
7633        // a public name colliding with a top-level type (`key` → `KeyFn` when a
7634        // `record Key` exists).
7635        let fn_name = if is_entry_point {
7636            to_camel_case(name)
7637        } else {
7638            self.go_fn_name(name)
7639        };
7640        let type_params = self.format_generic_params(generic_params);
7641        let param_strs = self.collect_param_strs(params);
7642        let effects = self.effects_params(effect_clause);
7643        let mut all_params = param_strs.clone();
7644        all_params.extend(effects.clone());
7645        let is_void = return_type.is_some_and(Self::is_void_type);
7646        let ret = if is_void {
7647            String::new()
7648        } else {
7649            return_type
7650                .map(|t| format!(" {}", self.type_to_go(t)))
7651                .unwrap_or_default()
7652        };
7653        if !effect_clause.is_empty() {
7654            let effect_names = self.expand_effect_names(effect_clause);
7655            self.fn_effects.insert(name.to_string(), effect_names);
7656        }
7657        self.writeln(&format!(
7658            "func {fn_name}{type_params}({}){ret} {{",
7659            all_params.join(", "),
7660        ));
7661        self.indent += 1;
7662        let old_handler_vars = self.current_handler_vars.clone();
7663        let expanded = self.expand_effect_names(effect_clause);
7664        for ename in &expanded {
7665            self.current_handler_vars
7666                .insert(ename.clone(), to_camel_case(ename));
7667        }
7668        let saved_record_args = self.var_record_type_args.clone();
7669        let saved_lambda_ret = self.var_lambda_ret.clone();
7670        let saved_decl_type = self.var_decl_type_node.clone();
7671        let (
7672            saved_opt_scope,
7673            saved_list_scope,
7674            saved_result_scope,
7675            saved_map_scope,
7676            saved_set_scope,
7677        ) = self.enter_param_optional_scope(params);
7678        // Record each typed param's Go type (`b: Box[T]` → `var_go_type["b"] =
7679        // "Box[T]"`) so a `value.field.get(i)` list receiver inside the body can
7680        // recover the field's `[]T` element type (GAP-A). `current_self_record`
7681        // already covers `self.field`; this covers a non-self generic-record
7682        // param. Restored alongside the other param scopes on exit.
7683        let saved_go_types = self.enter_param_go_types_with_expected(params, None);
7684        // Seed the function body's Go block frame with the parameter names so a
7685        // `let` that shadows a parameter (the same Go scope — the body gets no
7686        // extra brace) lowers to a reassignment, not a re-declaration.
7687        self.pending_scope_seed = Some(self.param_binding_names(params));
7688        if name == "main" || is_void {
7689            self.emit_block_body(body)?;
7690        } else {
7691            let prev_ret = self.current_fn_ret_type.take();
7692            let prev_ret_coll = self.current_fn_ret_collection_elem.take();
7693            let prev_ret_node = self.current_fn_ret_type_node.take();
7694            self.current_fn_ret_type = return_type.map(|t| self.type_to_go(t));
7695            self.current_fn_ret_collection_elem =
7696                return_type.and_then(|t| self.collection_elem_go_types(t));
7697            self.current_fn_ret_type_node = Self::fn_type_ret_node(return_type);
7698            self.emit_block_body_return(body)?;
7699            self.current_fn_ret_type = prev_ret;
7700            self.current_fn_ret_collection_elem = prev_ret_coll;
7701            self.current_fn_ret_type_node = prev_ret_node;
7702        }
7703        self.var_optional_elem = saved_opt_scope;
7704        self.var_list_elem = saved_list_scope;
7705        self.var_result_elem = saved_result_scope;
7706        self.var_map_kv = saved_map_scope;
7707        self.var_set_elem = saved_set_scope;
7708        self.var_go_type = saved_go_types;
7709        self.var_record_type_args = saved_record_args;
7710        self.var_lambda_ret = saved_lambda_ret;
7711        self.var_decl_type_node = saved_decl_type;
7712        self.current_handler_vars = old_handler_vars;
7713        self.indent -= 1;
7714        self.writeln("}");
7715
7716        // Async wrapper: every `async fn` gets a companion `FnAsync` that
7717        // starts a goroutine and returns a buffered `<-chan T` (or
7718        // `<-chan struct{}` for void returns). `main` is skipped — Go's
7719        // entry point is always `func main()` and wrapping it would be dead
7720        // code the linker would complain about.
7721        if is_async && name != "main" {
7722            self.buf.push('\n');
7723            self.emit_async_wrapper(
7724                &fn_name,
7725                &type_params,
7726                params,
7727                return_type,
7728                is_void,
7729                &effects,
7730            )?;
7731        }
7732        Ok(())
7733    }
7734
7735    /// Emit the `FnNameAsync` companion for an `async fn`. The wrapper starts
7736    /// a goroutine, invokes the sync body with the caller's arguments, and
7737    /// returns the result over a buffered channel. Callers `await`
7738    /// (= `<-chan T`) to observe completion.
7739    fn emit_async_wrapper(
7740        &mut self,
7741        sync_fn_name: &str,
7742        type_params: &str,
7743        params: &[AIRNode],
7744        return_type: Option<&AIRNode>,
7745        is_void: bool,
7746        effects: &[String],
7747    ) -> Result<(), CodegenError> {
7748        let async_fn_name = format!("{sync_fn_name}Async");
7749        let param_strs = self.collect_param_strs(params);
7750        let mut all_params = param_strs;
7751        all_params.extend(effects.iter().cloned());
7752        let chan_ty = if is_void {
7753            "struct{}".to_string()
7754        } else {
7755            return_type
7756                .map(|t| self.type_to_go(t))
7757                .unwrap_or_else(|| "interface{}".to_string())
7758        };
7759        self.writeln(&format!(
7760            "func {async_fn_name}{type_params}({}) <-chan {chan_ty} {{",
7761            all_params.join(", "),
7762        ));
7763        self.indent += 1;
7764        self.writeln(&format!("__ch := make(chan {chan_ty}, 1)"));
7765        self.writeln("go func() {");
7766        self.indent += 1;
7767        // Forward the sync function's arguments verbatim. Param names are
7768        // the camel-cased binding names the wrapper receives.
7769        let call_args: Vec<String> = params
7770            .iter()
7771            .filter_map(|p| {
7772                if let NodeKind::Param { pattern, .. } = &p.kind {
7773                    Some(self.pattern_to_binding_name(pattern))
7774                } else {
7775                    None
7776                }
7777            })
7778            .chain(effects.iter().map(|e| {
7779                // Effects params look like `name EffectType`; recover the
7780                // name before the first space.
7781                e.split_whitespace().next().unwrap_or("").to_string()
7782            }))
7783            .collect();
7784        let call_site = format!("{sync_fn_name}({})", call_args.join(", "));
7785        if is_void {
7786            self.writeln(&call_site);
7787            self.writeln("__ch <- struct{}{}");
7788        } else {
7789            self.writeln(&format!("__ch <- {call_site}"));
7790        }
7791        self.indent -= 1;
7792        self.writeln("}()");
7793        self.writeln("return __ch");
7794        self.indent -= 1;
7795        self.writeln("}");
7796        Ok(())
7797    }
7798
7799    fn emit_method(
7800        &mut self,
7801        receiver_type: &str,
7802        target_generics: &[bock_ast::GenericParam],
7803        method: &AIRNode,
7804        use_value_receiver: bool,
7805    ) -> Result<(), CodegenError> {
7806        // Inside ANY impl method (trait or plain inherent), a `Self` type
7807        // resolves to the concrete target (the receiver type) — whether it
7808        // appears in a synthesized trait default (`other: Self`) or an inherent
7809        // method's own signature (`fn combine(self, ...) -> Self`). Previously
7810        // this was gated on `use_value_receiver` (trait impls only), so an
7811        // inherent-impl `Self` lowered to the `/* Self */` placeholder and
7812        // produced an invalid Go signature. `receiver_type` is in scope for both
7813        // method kinds. Saved/restored so the ctx-wide default of `/* Self */`
7814        // is unchanged outside impl methods.
7815        let prev_self_subst = self.go_self_subst.take();
7816        self.go_self_subst = Some(receiver_type.to_string());
7817        // The base record name (`ListIter` from `ListIter[T]` / `ListIter`) so a
7818        // `self.field` list receiver inside the body resolves its element type
7819        // via `record_field_list_elem`. Restored on exit.
7820        let prev_self_record = self.current_self_record.take();
7821        let base = receiver_type
7822            .split_once('[')
7823            .map_or(receiver_type, |(b, _)| b)
7824            .to_string();
7825        self.current_self_record = Some(base);
7826        let result =
7827            self.emit_method_body(receiver_type, target_generics, method, use_value_receiver);
7828        self.current_self_record = prev_self_record;
7829        self.go_self_subst = prev_self_subst;
7830        result
7831    }
7832
7833    fn emit_method_body(
7834        &mut self,
7835        receiver_type: &str,
7836        target_generics: &[bock_ast::GenericParam],
7837        method: &AIRNode,
7838        use_value_receiver: bool,
7839    ) -> Result<(), CodegenError> {
7840        if let NodeKind::FnDecl {
7841            visibility,
7842            name,
7843            generic_params,
7844            params,
7845            return_type,
7846            effect_clause,
7847            body,
7848            ..
7849        } = &method.kind
7850        {
7851            // A trait-impl method (`use_value_receiver`) is PascalCased
7852            // regardless of Bock visibility: Go interface methods are always
7853            // exported (the `TraitDecl` emission PascalCases them), so the
7854            // receiver method and the call site must match. A `private` trait
7855            // default method (e.g. `not_equals`) would otherwise be camelCased
7856            // here while the interface declares it PascalCased. An inherent
7857            // method whose name a trait *also* declares (so it is in
7858            // `public_methods`) is exported too: it is the real implementation
7859            // that satisfies the interface (`impl Button { fn render }` →
7860            // `Render`), and every call site already PascalCases a
7861            // `public_methods` name — declaration and dispatch must agree.
7862            // Otherwise inherent methods keep the public/private casing rule.
7863            let is_public_method = use_value_receiver
7864                || matches!(visibility, Visibility::Public)
7865                || self.public_methods.contains(&name.name);
7866            let method_name = self.go_method_name(&name.name, is_public_method);
7867            // An associated function (no `self` receiver, e.g. a `From` impl's
7868            // `from`) has no Go-static equivalent: emit a free function named
7869            // `<Type>_<Method>` (reusing the DQ28 free-function naming) with no
7870            // receiver. The call site (`is_associated_call`) rewrites
7871            // `Type.method(args)` to the same `Type_Method(args)`.
7872            let receiver_base = receiver_type
7873                .split_once('[')
7874                .map_or(receiver_type, |(b, _)| b);
7875            if crate::generator::is_associated_impl_method(method, &self.effect_ops) {
7876                return self.emit_associated_fn(
7877                    receiver_base,
7878                    target_generics,
7879                    method,
7880                    is_public_method,
7881                );
7882            }
7883            // The AIR keeps `self` as a leading `Param` and method bodies refer
7884            // to `self.Field`. Name the Go receiver `self` and drop the leading
7885            // `self` param so the body resolves with no rewrite — otherwise the
7886            // receiver was `p` while the body referenced an undefined `self`,
7887            // and `self` also leaked in as a stray `interface{}` parameter.
7888            let (receiver_var, rest) = match params.first().map(crate::generator::param_binds_self)
7889            {
7890                Some(Some(_)) => ("self".to_string(), &params[1..]),
7891                _ => (
7892                    receiver_type
7893                        .chars()
7894                        .next()
7895                        .unwrap_or('r')
7896                        .to_lowercase()
7897                        .to_string(),
7898                    &params[..],
7899                ),
7900            };
7901            let param_strs = self.collect_param_strs(rest);
7902            let effects = self.effects_params(effect_clause);
7903            let mut all_params = param_strs;
7904            all_params.extend(effects);
7905            let is_void = return_type.as_deref().is_some_and(Self::is_void_type);
7906            let ret = if is_void {
7907                String::new()
7908            } else {
7909                return_type
7910                    .as_deref()
7911                    .map(|t| format!(" {}", self.type_to_go(t)))
7912                    .unwrap_or_default()
7913            };
7914            // DQ28: a method declaring its own type parameters (`Box[T].map[U]`)
7915            // is free-function-lowered — Go forbids method type params. Emit
7916            // `func Box_Map[T any, U any](self Box[T], ..) ..` (the receiver
7917            // becomes a leading `self` *parameter*; the receiver's and the
7918            // method's type params combine on the free function, which Go allows).
7919            // Every call site is rewritten to `Box_Map(box, ..)` by the call
7920            // emitter. A non-generic method keeps the idiomatic Go receiver form.
7921            let receiver_base = receiver_type
7922                .split_once('[')
7923                .map_or(receiver_type, |(b, _)| b);
7924            let freefn_lowered = !generic_params.is_empty()
7925                && self.freefn_lowered_type(&name.name) == Some(receiver_base);
7926            if freefn_lowered {
7927                // Combine receiver type params (`[T any]`) with the method's own
7928                // (`[U any]`) into one Go free-function type-param list.
7929                let mut combined = target_generics.to_vec();
7930                combined.extend(generic_params.iter().cloned());
7931                let type_params = self.format_generic_params(&combined);
7932                let receiver_args = self.format_generic_param_args(target_generics);
7933                let self_param = format!("{receiver_var} {receiver_type}{receiver_args}");
7934                let mut freefn_params = vec![self_param];
7935                freefn_params.extend(all_params);
7936                let fn_name = self.freefn_lowered_name(receiver_base, &name.name, is_public_method);
7937                self.writeln(&format!(
7938                    "func {fn_name}{type_params}({}){ret} {{",
7939                    freefn_params.join(", "),
7940                ));
7941            } else {
7942                let receiver_prefix = if use_value_receiver { "" } else { "*" };
7943                // Go binds a generic type's params on the receiver itself:
7944                // `func (self *Box[T]) ...`. The bare-name arg list (`[T]`) brings
7945                // `T` into scope for the receiver type, params, and body.
7946                let receiver_generics = self.format_generic_param_args(target_generics);
7947                self.writeln(&format!(
7948                    "func ({receiver_var} {receiver_prefix}{receiver_type}{receiver_generics}) \
7949                     {method_name}({}){ret} {{",
7950                    all_params.join(", "),
7951                ));
7952            }
7953            self.indent += 1;
7954            let old_handler_vars = self.current_handler_vars.clone();
7955            let expanded = self.expand_effect_names(effect_clause);
7956            for ename in &expanded {
7957                self.current_handler_vars
7958                    .insert(ename.clone(), to_camel_case(ename));
7959            }
7960            let saved_record_args = self.var_record_type_args.clone();
7961            let saved_decl_type = self.var_decl_type_node.clone();
7962            let (
7963                saved_opt_scope,
7964                saved_list_scope,
7965                saved_result_scope,
7966                saved_map_scope,
7967                saved_set_scope,
7968            ) = self.enter_param_optional_scope(rest);
7969            // Seed the method body's Go block frame with the receiver var and the
7970            // value parameter names (same Go scope as the body) so a shadowing
7971            // `let` reassigns rather than re-declares.
7972            let mut method_seed = self.param_binding_names(rest);
7973            method_seed.push(receiver_var.clone());
7974            self.pending_scope_seed = Some(method_seed);
7975            if return_type.is_some() && !is_void {
7976                let prev_ret = self.current_fn_ret_type.take();
7977                let prev_ret_coll = self.current_fn_ret_collection_elem.take();
7978                let prev_ret_node = self.current_fn_ret_type_node.take();
7979                self.current_fn_ret_type = return_type.as_deref().map(|t| self.type_to_go(t));
7980                self.current_fn_ret_collection_elem = return_type
7981                    .as_deref()
7982                    .and_then(|t| self.collection_elem_go_types(t));
7983                self.current_fn_ret_type_node = Self::fn_type_ret_node(return_type.as_deref());
7984                self.emit_block_body_return(body)?;
7985                self.current_fn_ret_type = prev_ret;
7986                self.current_fn_ret_collection_elem = prev_ret_coll;
7987                self.current_fn_ret_type_node = prev_ret_node;
7988            } else {
7989                self.emit_block_body(body)?;
7990            }
7991            self.var_optional_elem = saved_opt_scope;
7992            self.var_list_elem = saved_list_scope;
7993            self.var_result_elem = saved_result_scope;
7994            self.var_map_kv = saved_map_scope;
7995            self.var_set_elem = saved_set_scope;
7996            self.var_record_type_args = saved_record_args;
7997            self.var_decl_type_node = saved_decl_type;
7998            self.current_handler_vars = old_handler_vars;
7999            self.indent -= 1;
8000            self.writeln("}");
8001        }
8002        Ok(())
8003    }
8004
8005    /// Emit an impl/trait **associated function** (no `self` receiver) as a Go
8006    /// free function `func <Type>_<Method>(params) ret { ... }`.
8007    ///
8008    /// Go has no static methods, so an associated function — e.g. a `From` impl's
8009    /// `from(value) -> Self` — cannot attach to the type. It is emitted as a free
8010    /// function whose name carries the type prefix (`Foot_From`), matching the
8011    /// `Type.method(args)` → `Type_Method(args)` rewrite at the call site
8012    /// (`is_associated_call`). The `<Type>_` prefix keeps the name collision-free
8013    /// across types sharing a method name.
8014    fn emit_associated_fn(
8015        &mut self,
8016        receiver_base: &str,
8017        target_generics: &[bock_ast::GenericParam],
8018        method: &AIRNode,
8019        is_public_method: bool,
8020    ) -> Result<(), CodegenError> {
8021        let NodeKind::FnDecl {
8022            name,
8023            generic_params,
8024            params,
8025            return_type,
8026            effect_clause,
8027            body,
8028            ..
8029        } = &method.kind
8030        else {
8031            return Ok(());
8032        };
8033        let fn_name = self.freefn_lowered_name(receiver_base, &name.name, is_public_method);
8034        // Combine the target's type params with the method's own onto the free
8035        // function (Go forbids method type params, but a free function may carry
8036        // both — mirrors the DQ28 free-function lowering).
8037        let mut combined = target_generics.to_vec();
8038        combined.extend(generic_params.iter().cloned());
8039        let type_params = self.format_generic_params(&combined);
8040        let param_strs = self.collect_param_strs(params);
8041        let effects = self.effects_params(effect_clause);
8042        let mut all_params = param_strs;
8043        all_params.extend(effects);
8044        let is_void = return_type.as_deref().is_some_and(Self::is_void_type);
8045        let ret = if is_void {
8046            String::new()
8047        } else {
8048            return_type
8049                .as_deref()
8050                .map(|t| format!(" {}", self.type_to_go(t)))
8051                .unwrap_or_default()
8052        };
8053        self.writeln(&format!(
8054            "func {fn_name}{type_params}({}){ret} {{",
8055            all_params.join(", "),
8056        ));
8057        self.indent += 1;
8058        let old_handler_vars = self.current_handler_vars.clone();
8059        let expanded = self.expand_effect_names(effect_clause);
8060        for ename in &expanded {
8061            self.current_handler_vars
8062                .insert(ename.clone(), to_camel_case(ename));
8063        }
8064        let saved_record_args = self.var_record_type_args.clone();
8065        let saved_decl_type = self.var_decl_type_node.clone();
8066        let (
8067            saved_opt_scope,
8068            saved_list_scope,
8069            saved_result_scope,
8070            saved_map_scope,
8071            saved_set_scope,
8072        ) = self.enter_param_optional_scope(params);
8073        self.pending_scope_seed = Some(self.param_binding_names(params));
8074        if return_type.is_some() && !is_void {
8075            let prev_ret = self.current_fn_ret_type.take();
8076            let prev_ret_coll = self.current_fn_ret_collection_elem.take();
8077            let prev_ret_node = self.current_fn_ret_type_node.take();
8078            self.current_fn_ret_type = return_type.as_deref().map(|t| self.type_to_go(t));
8079            self.current_fn_ret_collection_elem = return_type
8080                .as_deref()
8081                .and_then(|t| self.collection_elem_go_types(t));
8082            self.current_fn_ret_type_node = Self::fn_type_ret_node(return_type.as_deref());
8083            self.emit_block_body_return(body)?;
8084            self.current_fn_ret_type = prev_ret;
8085            self.current_fn_ret_collection_elem = prev_ret_coll;
8086            self.current_fn_ret_type_node = prev_ret_node;
8087        } else {
8088            self.emit_block_body(body)?;
8089        }
8090        self.var_optional_elem = saved_opt_scope;
8091        self.var_list_elem = saved_list_scope;
8092        self.var_result_elem = saved_result_scope;
8093        self.var_map_kv = saved_map_scope;
8094        self.var_set_elem = saved_set_scope;
8095        self.var_record_type_args = saved_record_args;
8096        self.var_decl_type_node = saved_decl_type;
8097        self.current_handler_vars = old_handler_vars;
8098        self.indent -= 1;
8099        self.writeln("}");
8100        Ok(())
8101    }
8102
8103    fn collect_param_strs(&self, params: &[AIRNode]) -> Vec<String> {
8104        params
8105            .iter()
8106            .filter_map(|p| {
8107                if let NodeKind::Param { pattern, ty, .. } = &p.kind {
8108                    let name = self.pattern_to_binding_name(pattern);
8109                    let type_str = ty
8110                        .as_ref()
8111                        .map(|t| format!(" {}", self.type_to_go(t)))
8112                        .unwrap_or_else(|| " interface{}".into());
8113                    Some(format!("{name}{type_str}"))
8114                } else {
8115                    None
8116                }
8117            })
8118            .collect()
8119    }
8120
8121    fn collect_param_type_strs(&self, params: &[AIRNode]) -> Vec<String> {
8122        params
8123            .iter()
8124            .filter_map(|p| {
8125                if let NodeKind::Param { ty, .. } = &p.kind {
8126                    let type_str = ty
8127                        .as_ref()
8128                        .map(|t| self.type_to_go(t))
8129                        .unwrap_or_else(|| "interface{}".into());
8130                    Some(type_str)
8131                } else {
8132                    None
8133                }
8134            })
8135            .collect()
8136    }
8137
8138    /// Expand effect names, replacing composite effects with their components.
8139    fn expand_effect_names(&self, effects: &[bock_ast::TypePath]) -> Vec<String> {
8140        let mut result = Vec::new();
8141        for tp in effects {
8142            let name = tp
8143                .segments
8144                .last()
8145                .map_or("effect".to_string(), |s| s.name.clone());
8146            if let Some(components) = self.composite_effects.get(&name) {
8147                result.extend(components.iter().cloned());
8148            } else {
8149                result.push(name);
8150            }
8151        }
8152        result
8153    }
8154
8155    /// The in-scope `Clock` effect handler variable, if one is installed.
8156    ///
8157    /// When `Some`, the `Clock` time operations (`Instant.now`, `sleep`,
8158    /// `elapsed`) are routed through the handler instead of inlining the host
8159    /// primitive (Q-clock-handler-routing, §18.3.1/§18.4); when `None`, no
8160    /// handler is in scope and the default host primitive is emitted.
8161    fn clock_handler_var(&self) -> Option<&str> {
8162        self.current_handler_vars.get("Clock").map(String::as_str)
8163    }
8164
8165    /// Effects → interface parameters: `log Log, clock Clock`.
8166    fn effects_params(&self, effects: &[bock_ast::TypePath]) -> Vec<String> {
8167        let expanded = self.expand_effect_names(effects);
8168        expanded
8169            .iter()
8170            .map(|name| format!("{} {}", to_camel_case(name), name))
8171            .collect()
8172    }
8173
8174    /// Build `handler_var, ...` arguments for calling an effectful function.
8175    fn build_effects_call_args_go(&self, fn_name: &str) -> Option<String> {
8176        let effects = self.fn_effects.get(fn_name)?;
8177        let entries: Vec<String> = effects
8178            .iter()
8179            .filter_map(|e| {
8180                let handler_var = self.current_handler_vars.get(e)?;
8181                Some(handler_var.clone())
8182            })
8183            .collect();
8184        if entries.is_empty() {
8185            return None;
8186        }
8187        Some(entries.join(", "))
8188    }
8189
8190    // ── Enum variant structs ────────────────────────────────────────────────
8191
8192    fn emit_enum_variant(
8193        &mut self,
8194        enum_name: &str,
8195        generic_params: &[bock_ast::GenericParam],
8196        variant: &AIRNode,
8197    ) -> Result<(), CodegenError> {
8198        if let NodeKind::EnumVariant { name, payload } = &variant.kind {
8199            let vname = &name.name;
8200            let type_params = self.format_generic_params(generic_params);
8201            // The marker-method *receiver* type-param list must NOT carry the
8202            // `any` constraint: a Go receiver writes the bare `[T]` form
8203            // (`func (BoxFull[T]) isBox()`), whereas the constrained `[T any]`
8204            // form is a syntax error in receiver position ("unexpected name any,
8205            // expected ]"). The type DECLARATION above keeps the constrained
8206            // `[T any]` form; the receiver uses the bare-name args.
8207            let recv_type_params = self.format_generic_param_args(generic_params);
8208            match payload {
8209                EnumVariantPayload::Unit => {
8210                    self.writeln(&format!("type {enum_name}{vname}{type_params} struct{{}}"));
8211                }
8212                EnumVariantPayload::Struct(fields) => {
8213                    self.writeln(&format!("type {enum_name}{vname}{type_params} struct {{"));
8214                    self.indent += 1;
8215                    for f in fields {
8216                        let type_str = self.ast_type_to_go(&f.ty);
8217                        self.writeln(&format!("{}\t{type_str}", to_pascal_case(&f.name.name)));
8218                    }
8219                    self.indent -= 1;
8220                    self.writeln("}");
8221                }
8222                EnumVariantPayload::Tuple(elems) => {
8223                    self.writeln(&format!("type {enum_name}{vname}{type_params} struct {{"));
8224                    self.indent += 1;
8225                    for (i, elem) in elems.iter().enumerate() {
8226                        let type_str = self.type_to_go(elem);
8227                        self.writeln(&format!("Field{i}\t{type_str}"));
8228                    }
8229                    self.indent -= 1;
8230                    self.writeln("}");
8231                }
8232            }
8233            // Implement the interface marker method. The receiver uses the
8234            // bare `[T]` type-param form (`recv_type_params`), never `[T any]`.
8235            self.buf.push('\n');
8236            self.writeln(&format!(
8237                "func ({enum_name}{vname}{recv_type_params}) is{enum_name}() {{}}"
8238            ));
8239        }
8240        Ok(())
8241    }
8242
8243    // ── Statements ──────────────────────────────────────────────────────────
8244
8245    fn emit_stmt(&mut self, node: &AIRNode) -> Result<(), CodegenError> {
8246        match &node.kind {
8247            NodeKind::LetBinding {
8248                pattern, value, ty, ..
8249            } => {
8250                // Declare-only temp from the shared value-CF hoist: Go needs a
8251                // type for `var x T`. The owning block pre-scanned this temp
8252                // against its following control-flow statement and recorded the
8253                // inferred Go type in `decl_only_types`; emit `var x T`. A typed
8254                // annotation, if present, wins. Falls back to `interface{}` when
8255                // the type cannot be inferred (the relocated CF still assigns it).
8256                if node.metadata.contains_key(crate::generator::DECL_ONLY_META) {
8257                    let binding = self.pattern_to_go_binding(pattern);
8258                    let type_str = ty
8259                        .as_ref()
8260                        .map(|t| self.type_to_go(t))
8261                        .or_else(|| self.decl_only_types.get(&binding).cloned())
8262                        .unwrap_or_else(|| "interface{}".to_string());
8263                    self.var_go_type.insert(binding.clone(), type_str.clone());
8264                    let ind = self.indent_str();
8265                    let _ = writeln!(self.buf, "{ind}var {binding} {type_str}");
8266                    return Ok(());
8267                }
8268                // `let x = expr?` — the `?` operand is unwrapped (or the enclosing
8269                // function returns early) into a temp, then bound to `x`. Handled
8270                // before the general `let` paths so the early-return statements are
8271                // emitted at statement position (Go has no expression-level
8272                // early-return). Only a plain `BindPat` target is handled here; a
8273                // destructuring `let (a, b) = expr?` is rare and falls through.
8274                if let NodeKind::Propagate { expr: inner } = &value.kind {
8275                    if matches!(pattern.kind, NodeKind::BindPat { .. }) {
8276                        let binding = self.pattern_to_go_binding(pattern);
8277                        let payload = self.emit_try_unwrap(inner)?;
8278                        let ind = self.indent_str();
8279                        let _ = writeln!(self.buf, "{ind}{binding} := {payload}");
8280                        self.writeln(&format!("_ = {binding}"));
8281                        // Record the Ok element type so a later typed use of the
8282                        // binding resolves, and track it for shadowing-`let`.
8283                        if let Some((ok, _)) = self.scrutinee_result_elems(inner) {
8284                            self.var_go_type.insert(binding.clone(), ok);
8285                        } else if let Some(elem) = self.scrutinee_optional_elem(inner) {
8286                            self.var_go_type.insert(binding.clone(), elem);
8287                        }
8288                        if binding != "_" {
8289                            self.go_record_declared(&binding);
8290                        }
8291                        return Ok(());
8292                    }
8293                }
8294                // Tuple-destructuring `let (a, b, …) = expr`. Go has no tuple
8295                // destructuring and `pattern_to_binding_name` collapses a tuple
8296                // pattern to its *first* element — so without this every later
8297                // name was dropped (`undefined: total`) and the first bound the
8298                // whole struct. Hoist the value into a `__tupN` struct local and
8299                // bind each element off its `.Field{i}` (recursing through the
8300                // shared bind emitter for a nested element pattern). A bare-`_`
8301                // tuple pattern still binds nothing.
8302                if let NodeKind::TuplePat { elems } = &pattern.kind {
8303                    let n = self.let_tuple_counter;
8304                    self.let_tuple_counter += 1;
8305                    let tmp = format!("__tup{n}");
8306                    let ind = self.indent_str();
8307                    let _ = write!(self.buf, "{ind}{tmp} := ");
8308                    // The declared tuple type (when annotated) types each field;
8309                    // otherwise the struct literal/return value carries its own
8310                    // Go types and the `.Field{i}` reads inherit them.
8311                    self.emit_expr(value)?;
8312                    self.buf.push('\n');
8313                    self.writeln(&format!("_ = {tmp}"));
8314                    let decl_ty = ty.as_deref();
8315                    let field_tys = self.tuple_field_decl_tys(decl_ty, elems.len());
8316                    for (i, e) in elems.iter().enumerate() {
8317                        let access = format!("{tmp}.Field{i}");
8318                        let mut binds = String::new();
8319                        self.collect_binds_go(
8320                            e,
8321                            &access,
8322                            field_tys.get(i).and_then(|t| *t),
8323                            &mut binds,
8324                        );
8325                        for stmt in binds.split("; ") {
8326                            let stmt = stmt.trim();
8327                            if stmt.is_empty() {
8328                                continue;
8329                            }
8330                            self.writeln(stmt);
8331                            if let Some(name) = stmt.split_whitespace().next() {
8332                                if name != "_" {
8333                                    self.go_record_declared(name);
8334                                }
8335                            }
8336                        }
8337                    }
8338                    return Ok(());
8339                }
8340                let binding = self.pattern_to_go_binding(pattern);
8341                // Shadowing re-bind of a name already declared in this Go block
8342                // (the immutable-update idiom, `let acc = …; let acc = f(acc)`,
8343                // and the todo-list example's `let list = list.add(…)`). Go's
8344                // `:=` / `var` reject a re-declaration ("no new variables on left
8345                // side of :="), so lower it to a plain assignment `acc = …`. Only
8346                // a simple `BindPat` participates — a tuple/record destructure or
8347                // `_` is not the rebind idiom and keeps its declaration. The
8348                // existing `var_*`-scope recording below is skipped for a reassign
8349                // (the name's type is fixed by its first declaration), but the
8350                // value's expected-type hint is preserved so a branchy RHS still
8351                // lowers to a correctly-typed IIFE.
8352                if matches!(pattern.kind, NodeKind::BindPat { .. })
8353                    && binding != "_"
8354                    && self.go_name_declared_in_block(&binding)
8355                {
8356                    let ind = self.indent_str();
8357                    let _ = write!(self.buf, "{ind}{binding} = ");
8358                    let prev_expected_type = self.current_expected_type.take();
8359                    if let Some(existing) = self.var_go_type.get(&binding) {
8360                        self.current_expected_type = Some(existing.clone());
8361                    }
8362                    let prev_expected = self.expected_collection_elem.take();
8363                    if matches!(
8364                        value.kind,
8365                        NodeKind::ListLiteral { .. }
8366                            | NodeKind::MapLiteral { .. }
8367                            | NodeKind::SetLiteral { .. }
8368                    ) {
8369                        if let Some(t) = ty {
8370                            self.expected_collection_elem = self.collection_elem_go_types(t);
8371                        }
8372                    }
8373                    self.emit_expr(value)?;
8374                    self.current_expected_type = prev_expected_type;
8375                    self.expected_collection_elem = prev_expected;
8376                    self.buf.push('\n');
8377                    return Ok(());
8378                }
8379                if let Some(t) = ty {
8380                    // Record the full declared type node so a later `match` on
8381                    // this binding can peel a *nested* Optional/Result to assert
8382                    // a tuple payload to its concrete struct (mirrors the param
8383                    // path; see `var_decl_type_node`).
8384                    self.var_decl_type_node
8385                        .insert(self.pattern_to_binding_name(pattern), (**t).clone());
8386                    // Record an `Optional[T]` binding's element type so a later
8387                    // `match binding { Some(x) => ... }` can type-assert `x`.
8388                    if let Some(elem) = self.optional_elem_go_type(t) {
8389                        self.var_optional_elem
8390                            .insert(self.pattern_to_binding_name(pattern), elem);
8391                    }
8392                    // Record a `List[T]` binding's element type so a later
8393                    // `match binding.get(i) { Some(x) => ... }` can type-assert
8394                    // the `interface{}` payload.
8395                    if let Some(elem) = self.list_elem_go_type(t) {
8396                        self.var_list_elem
8397                            .insert(self.pattern_to_binding_name(pattern), elem);
8398                    }
8399                    // Record a `Map[K, V]` / `Set[E]` binding's element Go types
8400                    // so a later built-in method (`m.get(k)`, `s.contains(x)`,
8401                    // …) lowers its inline closures over the concretely-typed
8402                    // receiver `map[K]V` / `map[E]struct{}`.
8403                    if let Some(kv) = self.map_kv_go_types(t) {
8404                        self.var_map_kv
8405                            .insert(self.pattern_to_binding_name(pattern), kv);
8406                    }
8407                    if let Some(elem) = self.set_elem_go_type(t) {
8408                        self.var_set_elem
8409                            .insert(self.pattern_to_binding_name(pattern), elem);
8410                    }
8411                    // Record a `Result[T, E]` binding's Ok/Err types so a later
8412                    // `match binding { Ok(v) => ...; Err(e) => ... }` can
8413                    // type-assert the bound payload.
8414                    if let Some(elems) = self.result_elem_go_types(t) {
8415                        self.var_result_elem
8416                            .insert(self.pattern_to_binding_name(pattern), elems);
8417                    }
8418                    // Record a generic-record binding's concrete instantiation
8419                    // (`let c: ListIter[Int]` → `("ListIter", ["int64"])`) so a
8420                    // later `match c.next() { Some(x) => ... }` resolves the
8421                    // generic `Optional[T]` payload to the concrete arg (`int64`)
8422                    // rather than the undefined-in-caller `T`.
8423                    if let Some(record_args) = self.record_type_args(t) {
8424                        self.var_record_type_args
8425                            .insert(self.pattern_to_binding_name(pattern), record_args);
8426                    }
8427                    let type_str = self.type_to_go(t);
8428                    // Record the binding's rendered Go type so a later use as a
8429                    // call argument (`max_of(noKeys)` where `noKeys: List[Key]`)
8430                    // resolves to `[]Key`, letting a generic callee bind its
8431                    // element type from the argument rather than collapsing to
8432                    // `[any]`. Function-scoped: params save/restore `var_go_type`.
8433                    if let NodeKind::BindPat { name, .. } = &pattern.kind {
8434                        self.var_go_type
8435                            .insert(go_value_ident(&name.name), type_str.clone());
8436                    }
8437                    let ind = self.indent_str();
8438                    let _ = write!(self.buf, "{ind}var {binding} {type_str} = ");
8439                    // When the binding *value* is itself a collection literal,
8440                    // it takes its element type(s) from the declared type, so an
8441                    // empty `[]` (or under-inferred literal) matches the declared
8442                    // `[]T` / `map[K]V` rather than falling back to
8443                    // `[]interface{}`. Guarded to the top-level literal so the
8444                    // hint never leaks to a nested/argument literal whose own
8445                    // type may differ.
8446                    let prev_expected = self.expected_collection_elem.take();
8447                    if matches!(
8448                        value.kind,
8449                        NodeKind::ListLiteral { .. }
8450                            | NodeKind::MapLiteral { .. }
8451                            | NodeKind::SetLiteral { .. }
8452                    ) {
8453                        self.expected_collection_elem = self.collection_elem_go_types(t);
8454                    }
8455                    // The binding's declared Go type is the expected type for the
8456                    // value expression. An expression-position `match`/`if` lowers
8457                    // to an IIFE whose return must be this `T` (not the enclosing
8458                    // function's return type), so `let x: T = match …` is
8459                    // assignable. Restored after the value so it never leaks.
8460                    let prev_expected_type = self.current_expected_type.take();
8461                    self.current_expected_type = Some(type_str.clone());
8462                    self.emit_expr(value)?;
8463                    self.current_expected_type = prev_expected_type;
8464                    self.expected_collection_elem = prev_expected;
8465                    self.buf.push('\n');
8466                } else {
8467                    // Propagate a `Map`/`Set` element type onto an untyped
8468                    // binding whose value returns a map/set (`let m2 =
8469                    // base.set(k, v)`, `let s2 = s.add(x)`), so a later built-in
8470                    // on `m2`/`s2` lowers its inline closure over the concrete
8471                    // `map[K]V` / `map[E]struct{}` rather than `interface{}`.
8472                    if let Some(kv) = self.value_map_kv_go_types(value) {
8473                        self.var_map_kv
8474                            .insert(self.pattern_to_binding_name(pattern), kv);
8475                    }
8476                    if let Some(elem) = self.value_set_elem_go_type(value) {
8477                        self.var_set_elem
8478                            .insert(self.pattern_to_binding_name(pattern), elem);
8479                    }
8480                    // Record an untyped binding's concrete generic-record args
8481                    // when its value is a call returning one (`__it := bag.Iter()`
8482                    // → `("ListIterator", ["int64"])`), so a later
8483                    // `match __it.next() { Some(x) => ... }` asserts the payload
8484                    // to the concrete arg. This is the `for x in <Iterable>`
8485                    // desugar case, whose gensym binding carries no annotation.
8486                    if let Some(record_args) = self.value_record_type_args(value) {
8487                        self.var_record_type_args
8488                            .insert(self.pattern_to_binding_name(pattern), record_args);
8489                    }
8490                    // Record an untyped `Result`-typed binding's `(ok, err)` Go
8491                    // payload types when its value is a call to a `Result`-returning
8492                    // fn (`step1 := eval(...)`) or a bare `Ok(v)`/`Err(e)`. Without
8493                    // this, a later `match step1 { Ok(v) => ... }` binds `v` from the
8494                    // boxed `interface{}` payload un-asserted, leaving `v` as
8495                    // `interface{}` and failing a downstream use that expects the
8496                    // concrete type (e.g. `eval(OpMul, v, 2.0)` wanting `float64`).
8497                    if let Some(elems) = self.value_result_elem_go_types(value) {
8498                        self.var_result_elem
8499                            .insert(self.pattern_to_binding_name(pattern), elems);
8500                    }
8501                    // Record an untyped `Optional`-typed binding's element Go type
8502                    // when its value is a call/method returning `Optional[T]`
8503                    // (`raw := storage.read(key)`). Reuses the scrutinee resolver,
8504                    // which already maps an effect-/user-method's `Optional[T]`
8505                    // return to its concrete element. Without this, a later
8506                    // `match raw { Some(v) => ... }` binds `v` from the boxed
8507                    // `interface{}` un-asserted, so a typed-IIFE arm (`return v`
8508                    // where `T` is `string`) fails the Go build.
8509                    if let Some(elem) = self.scrutinee_optional_elem(value) {
8510                        self.var_optional_elem
8511                            .insert(self.pattern_to_binding_name(pattern), elem);
8512                    }
8513                    // Record an untyped `List`-typed binding's Go element type — a
8514                    // homogeneous list literal (`let items = [Item{1}, Item{2}]` →
8515                    // `Item`) or a list-combinator result (`let updated =
8516                    // items.map(..)`, `let evens = xs.filter(..)`). Without this a
8517                    // later `updated.map((it) => …)` cannot type the closure param
8518                    // `it` to `Item` (it falls back to `interface{}`, so `it.title`
8519                    // and the `[]Item` result both fail), and a use of the binding
8520                    // as a typed call argument erases to `[]interface{}`.
8521                    if let Some(elem) = self.value_list_elem_go_type(value) {
8522                        let name = self.pattern_to_binding_name(pattern);
8523                        self.var_list_elem.insert(name.clone(), elem.clone());
8524                        self.var_go_type.insert(name, format!("[]{elem}"));
8525                    }
8526                    // Record an untyped binding to a record-returning value (`let
8527                    // form = create_form()` → `FormState`), so a later built-in
8528                    // collection method on one of its fields (`form.fields.keys()`,
8529                    // `form.fields.get(k)`) resolves the field's declared `Map[K,
8530                    // V]` / `List[T]` types through `map_receiver_kv_go_types` /
8531                    // the list analogue rather than erasing to the
8532                    // `map[interface{}]interface{}` / `[]interface{}` Go rejects
8533                    // against the concretely-typed struct field. Scoped to records
8534                    // that actually have such a field (the only consumers), so this
8535                    // never over-records a binding's Go type.
8536                    if !matches!(value.kind, NodeKind::Lambda { .. }) {
8537                        if let Some(go_ty) = self.infer_go_expr_type(value) {
8538                            let head = Self::go_type_record_head(&go_ty);
8539                            if self.record_field_map_kv.contains_key(head)
8540                                || self.record_field_list_elem.contains_key(head)
8541                            {
8542                                self.var_go_type
8543                                    .insert(self.pattern_to_binding_name(pattern), go_ty);
8544                            }
8545                        }
8546                    }
8547                    // Record an untyped binding to a lambda → the lambda's inferred
8548                    // Go return type, so a later compose `f >> binding` can resolve
8549                    // its own output type from `binding` (the outer local lambda).
8550                    if let NodeKind::Lambda { params, body } = &value.kind {
8551                        let saved = self.enter_param_go_types_with_expected(params, None);
8552                        let ret = self.infer_block_tail_type(body);
8553                        self.var_go_type = saved;
8554                        if let Some(r) = ret {
8555                            self.var_lambda_ret
8556                                .insert(self.pattern_to_binding_name(pattern), r);
8557                        }
8558                    }
8559                    // An untyped `let m = if (..) { Text } else { Image }` lowers
8560                    // its value to an expression IIFE. Without an expected type the
8561                    // IIFE falls back to the enclosing fn's return type
8562                    // (`current_fn_ret_type`, e.g. `__bockResult` inside a
8563                    // `Result`-returning fn), which a user-enum variant value
8564                    // (`MessageType`) is not assignable to. Infer the value's Go
8565                    // type structurally (the enum a variant branch/arm yields) and
8566                    // record it as the binding's expected type so the IIFE is typed
8567                    // `func() MessageType { … }`. Scoped to the value emit; never
8568                    // leaks. Only `if`/`match` values need this — a direct value
8569                    // emit is already concretely typed.
8570                    let prev_expected_type = self.current_expected_type.take();
8571                    if matches!(value.kind, NodeKind::If { .. } | NodeKind::Match { .. }) {
8572                        if let Some(inferred) = self.infer_branchy_expr_type(value) {
8573                            self.current_expected_type = Some(inferred.clone());
8574                            // Also record it for the binding so a later use of the
8575                            // binding (e.g. as a struct field) resolves to the enum.
8576                            self.var_go_type
8577                                .insert(self.pattern_to_binding_name(pattern), inferred);
8578                        }
8579                    }
8580                    let ind = self.indent_str();
8581                    // Bock `Int` is `int64`, but Go infers an *untyped integer
8582                    // constant* (`0`, `lo + 1`) as the default `int` under `:=`.
8583                    // A later mix with an `int64` value (`i >= int64(len(xs))`,
8584                    // `total + p` where `p: Int`, or a struct field `Level int64`)
8585                    // then fails `mismatched types int and int64`. When the value's
8586                    // structural Go type is `int64`, pin the binding with an explicit
8587                    // `var x int64 = …` and record it so downstream uses agree. Only
8588                    // `int64` needs this — `float64`/`bool`/`string` literals already
8589                    // infer to the matching Go type under `:=`. Skipped when the value
8590                    // is an `if`/`match`/`loop` (those lower to a typed IIFE) or a
8591                    // collection/record literal (handled above), to keep the targeted
8592                    // surface minimal.
8593                    //
8594                    // Restricted to value kinds whose `int64` Go type is *reliable*:
8595                    // an integer literal, integer arithmetic (`BinaryOp`/`UnaryOp`),
8596                    // or a known-`int64` identifier. A *call* is deliberately
8597                    // excluded — a generic fn (`firstOr[T](single(9), -1)`)
8598                    // monomorphizes `T` to Go's untyped-constant default `int`, so
8599                    // its *actual* return type is `int`, not the `int64`
8600                    // `infer_go_expr_type` predicts from the Bock `Int`; pinning
8601                    // `var x int64` there fails `cannot use … (int) as int64`.
8602                    let pin_int64_kind = matches!(
8603                        value.kind,
8604                        NodeKind::Literal { .. }
8605                            | NodeKind::BinaryOp { .. }
8606                            | NodeKind::UnaryOp { .. }
8607                            | NodeKind::Identifier { .. }
8608                    );
8609                    let pin_int64 = pin_int64_kind
8610                        && self.infer_go_expr_type(value).as_deref() == Some("int64");
8611                    if pin_int64 {
8612                        self.var_go_type
8613                            .insert(self.pattern_to_binding_name(pattern), "int64".to_string());
8614                        let _ = write!(self.buf, "{ind}var {binding} int64 = ");
8615                    } else {
8616                        let _ = write!(self.buf, "{ind}{binding} := ");
8617                    }
8618                    self.emit_expr(value)?;
8619                    self.current_expected_type = prev_expected_type;
8620                    self.buf.push('\n');
8621                }
8622                // Record this name as declared in the current Go block scope so a
8623                // later same-name `let` in the same block reassigns (see the
8624                // shadowing short-circuit above). Only a simple `BindPat`
8625                // introduces a tracked declaration.
8626                if matches!(pattern.kind, NodeKind::BindPat { .. }) && binding != "_" {
8627                    self.go_record_declared(&binding);
8628                }
8629                Ok(())
8630            }
8631            NodeKind::If {
8632                let_pattern,
8633                condition,
8634                then_block,
8635                else_block,
8636            } => {
8637                if let Some(pat) = let_pattern {
8638                    let binding = self.pattern_to_go_binding(pat);
8639                    let ind = self.indent_str();
8640                    let _ = write!(self.buf, "{ind}{binding} := ");
8641                    self.emit_expr(condition)?;
8642                    self.buf.push('\n');
8643                    self.writeln(&format!("if {binding} != nil {{"));
8644                    self.indent += 1;
8645                    self.emit_block_body(then_block)?;
8646                    self.indent -= 1;
8647                } else {
8648                    let ind = self.indent_str();
8649                    let _ = write!(self.buf, "{ind}if ");
8650                    self.emit_expr(condition)?;
8651                    self.buf.push_str(" {\n");
8652                    self.indent += 1;
8653                    self.emit_block_body(then_block)?;
8654                    self.indent -= 1;
8655                }
8656                if let Some(else_b) = else_block {
8657                    if matches!(else_b.kind, NodeKind::If { .. }) {
8658                        let ind = self.indent_str();
8659                        let _ = write!(self.buf, "{ind}}} else ");
8660                        // Emit the if without leading indent.
8661                        self.emit_if_continued(else_b)?;
8662                        return Ok(());
8663                    }
8664                    self.writeln("} else {");
8665                    self.indent += 1;
8666                    self.emit_block_body(else_b)?;
8667                    self.indent -= 1;
8668                }
8669                self.writeln("}");
8670                Ok(())
8671            }
8672            NodeKind::For {
8673                pattern,
8674                iterable,
8675                body,
8676            } => {
8677                let mut binding = self.pattern_to_go_binding(pattern);
8678                // Go rejects a `range` loop variable that is never read
8679                // (`declared and not used`), which Bock permits (`for x in data {
8680                // count = count + 1 }`). When the bound name is a plain
8681                // identifier not referenced in the body, emit `_` instead.
8682                if let NodeKind::BindPat { name, .. } = &pattern.kind {
8683                    let go_name = go_value_ident(&name.name);
8684                    if go_name == binding && !collect_used_idents(body).contains(&name.name) {
8685                        binding = "_".to_string();
8686                    }
8687                }
8688                self.emit_loop_label_prefix(body);
8689                let ind = self.indent_str();
8690                // `for _, _ := range x` is invalid Go ("no new variables on left
8691                // side of :="); when the value var is discarded too, drop the
8692                // assignment entirely (`for range x`).
8693                if binding == "_" {
8694                    let _ = write!(self.buf, "{ind}for range ");
8695                } else {
8696                    let _ = write!(self.buf, "{ind}for _, {binding} := range ");
8697                }
8698                self.emit_expr(iterable)?;
8699                self.buf.push_str(" {\n");
8700                self.indent += 1;
8701                // Record the loop variable's element Go type into the body scope
8702                // so element arithmetic / typed returns type-check (Go ranges
8703                // over a concretely-typed `[]T` / range yield a `T`, not
8704                // `interface{}`). Recoverable when the iterable is a known
8705                // `List[T]` identifier, a homogeneously-typed list literal, or a
8706                // range (`int64`). Saved/restored around the body — Go has no
8707                // block-scoped reset here. Unrecoverable ⇒ left absent, so
8708                // inference yields the `interface{}` fallback, never a wrong
8709                // type.
8710                let saved_go_types = self.var_go_type.clone();
8711                if let (NodeKind::BindPat { name, .. }, Some(elem)) =
8712                    (&pattern.kind, self.for_loop_elem_go_type(iterable))
8713                {
8714                    self.var_go_type.insert(go_value_ident(&name.name), elem);
8715                }
8716                self.emit_block_body(body)?;
8717                self.var_go_type = saved_go_types;
8718                self.indent -= 1;
8719                self.writeln("}");
8720                self.loop_labels.pop();
8721                Ok(())
8722            }
8723            NodeKind::While { condition, body } => {
8724                self.emit_loop_label_prefix(body);
8725                let ind = self.indent_str();
8726                let _ = write!(self.buf, "{ind}for ");
8727                self.emit_expr(condition)?;
8728                self.buf.push_str(" {\n");
8729                self.indent += 1;
8730                self.emit_block_body(body)?;
8731                self.indent -= 1;
8732                self.writeln("}");
8733                self.loop_labels.pop();
8734                Ok(())
8735            }
8736            NodeKind::Loop { body } => {
8737                // A statement-position `loop` is a plain `for {}`. Its body is no
8738                // longer the value-IIFE body of any enclosing expression-`loop`, so
8739                // reset `loop_expr_depth` for the body: a `break <v>` here is the
8740                // (value-dropping) statement case, not a `return`.
8741                let saved_loop_expr = self.loop_expr_depth;
8742                self.loop_expr_depth = 0;
8743                self.emit_loop_label_prefix(body);
8744                self.writeln("for {");
8745                self.indent += 1;
8746                self.emit_block_body(body)?;
8747                self.indent -= 1;
8748                self.writeln("}");
8749                self.loop_labels.pop();
8750                self.loop_expr_depth = saved_loop_expr;
8751                Ok(())
8752            }
8753            NodeKind::Return { value } => {
8754                if let Some(val) = value {
8755                    let ind = self.indent_str();
8756                    let _ = write!(self.buf, "{ind}return ");
8757                    // A collection literal in return position adopts the
8758                    // function's return collection element type(s), so `return
8759                    // [x]` in `fn single[T](x: T) -> List[T]` emits `[]T{x}` (not
8760                    // the `[]interface{}{x}` bare-literal inference falls back to,
8761                    // which is not assignable to the `[]T` return). Guarded to a
8762                    // top-level collection literal and consumed at the literal so
8763                    // it never leaks to a nested/argument literal.
8764                    let prev_expected = self.expected_collection_elem.take();
8765                    if matches!(
8766                        val.kind,
8767                        NodeKind::ListLiteral { .. }
8768                            | NodeKind::MapLiteral { .. }
8769                            | NodeKind::SetLiteral { .. }
8770                    ) {
8771                        self.expected_collection_elem = self.current_fn_ret_collection_elem.clone();
8772                    }
8773                    // A generic-record construction in explicit-`return` position
8774                    // adopts the function's return type for its args (see
8775                    // `emit_block_body_inner`'s tail-return arm).
8776                    let prev_expected_type = self.current_expected_type.take();
8777                    if matches!(
8778                        val.kind,
8779                        NodeKind::RecordConstruct { .. } | NodeKind::TupleLiteral { .. }
8780                    ) {
8781                        self.current_expected_type = self.current_fn_ret_type.clone();
8782                    }
8783                    self.emit_expr(val)?;
8784                    self.expected_collection_elem = prev_expected;
8785                    self.current_expected_type = prev_expected_type;
8786                    self.buf.push('\n');
8787                } else {
8788                    self.writeln("return");
8789                }
8790                Ok(())
8791            }
8792            NodeKind::Break { value } => {
8793                // Inside an expression-position `loop` IIFE, `break <v>` is the
8794                // loop's value: lower it to `return <v>` (out of the IIFE). The
8795                // value-dropping `// break value:` comment below would otherwise
8796                // discard it and leave the IIFE with no return at all.
8797                if self.loop_expr_depth > 0 {
8798                    if let Some(val) = value {
8799                        let ind = self.indent_str();
8800                        let _ = write!(self.buf, "{ind}return ");
8801                        self.emit_expr(val)?;
8802                        self.buf.push('\n');
8803                        return Ok(());
8804                    }
8805                    // A bare `break` inside a value `loop` cannot produce the
8806                    // loop's value; fall through to the ordinary `break` below
8807                    // (the IIFE's trailing `panic` covers the unreachable tail).
8808                }
8809                if let Some(val) = value {
8810                    let ind = self.indent_str();
8811                    let _ = write!(self.buf, "{ind}// break value: ");
8812                    self.emit_expr(val)?;
8813                    self.buf.push('\n');
8814                }
8815                // Inside a statement-arm `switch`, a bare `break` would exit
8816                // the switch; target the enclosing loop's label instead.
8817                if self.switch_label_depth > 0 {
8818                    if let Some(label) = self.innermost_loop_label() {
8819                        self.writeln(&format!("break {label}"));
8820                        return Ok(());
8821                    }
8822                }
8823                self.writeln("break");
8824                Ok(())
8825            }
8826            NodeKind::Continue => {
8827                // `continue` already targets the loop even from inside a switch,
8828                // but use the label when one is in scope for symmetry/clarity.
8829                if self.switch_label_depth > 0 {
8830                    if let Some(label) = self.innermost_loop_label() {
8831                        self.writeln(&format!("continue {label}"));
8832                        return Ok(());
8833                    }
8834                }
8835                self.writeln("continue");
8836                Ok(())
8837            }
8838            NodeKind::Guard {
8839                let_pattern,
8840                condition,
8841                else_block,
8842            } => {
8843                if let Some(pat) = let_pattern {
8844                    return self.emit_guard_let(pat, condition, else_block);
8845                }
8846                let ind = self.indent_str();
8847                let _ = write!(self.buf, "{ind}if !(");
8848                self.emit_expr(condition)?;
8849                self.buf.push_str(") {\n");
8850                self.indent += 1;
8851                self.emit_block_body(else_block)?;
8852                self.indent -= 1;
8853                self.writeln("}");
8854                Ok(())
8855            }
8856            NodeKind::Match { scrutinee, arms } => self.emit_match(scrutinee, arms),
8857            NodeKind::Block { stmts, tail } => {
8858                self.seed_decl_only_types(stmts);
8859                for s in stmts {
8860                    self.emit_node(s)?;
8861                }
8862                if let Some(t) = tail {
8863                    self.write_indent();
8864                    self.emit_expr(t)?;
8865                    self.buf.push('\n');
8866                }
8867                Ok(())
8868            }
8869            NodeKind::HandlingBlock { handlers, body } => {
8870                // handling block → scoped handler instantiation. The emitted
8871                // `{ … }` is its own Go block scope, so it gets a fresh
8872                // `go_declared_scopes` frame: a name first bound in one
8873                // `handling` block and re-bound in a *sibling* `handling` block
8874                // is two independent declarations (each block-scoped), not a
8875                // redeclaration. Without a fresh frame the redeclaration tracker
8876                // would carry the prior block's `declared` set into this one and
8877                // rewrite the second `let part = …` into a bare `part = …` — a
8878                // name that left scope when the first block closed (Go rejects it
8879                // as `undefined: part`). Mirrors the js/ts fix
8880                // (Q-js-handling-let-redeclaration, #371) on the Go backend
8881                // (Q-go-handling-let-redeclaration).
8882                self.writeln("{");
8883                self.indent += 1;
8884                self.go_declared_scopes.push(HashSet::new());
8885                let old_handler_vars = self.current_handler_vars.clone();
8886                let mut new_var_names = Vec::with_capacity(handlers.len());
8887                for h in handlers {
8888                    let effect_name = h
8889                        .effect
8890                        .segments
8891                        .last()
8892                        .map_or("effect", |s| s.name.as_str());
8893                    let var_name = format!("__{}", to_camel_case(effect_name));
8894                    let ind = self.indent_str();
8895                    let _ = write!(self.buf, "{ind}{var_name} := ");
8896                    self.emit_expr(&h.handler)?;
8897                    self.buf.push('\n');
8898                    self.current_handler_vars
8899                        .insert(effect_name.to_string(), var_name.clone());
8900                    new_var_names.push(var_name);
8901                }
8902                // Suppress Go's "declared but not used" error when a handler
8903                // is declared in an outer handling scope and only referenced
8904                // indirectly through inner handling blocks (or not at all).
8905                for v in &new_var_names {
8906                    self.writeln(&format!("_ = {v}"));
8907                }
8908                if let NodeKind::Block { stmts, tail } = &body.kind {
8909                    self.seed_decl_only_types(stmts);
8910                    for s in stmts {
8911                        self.emit_node(s)?;
8912                    }
8913                    if let Some(t) = tail {
8914                        self.write_indent();
8915                        self.emit_expr(t)?;
8916                        self.buf.push('\n');
8917                    }
8918                } else {
8919                    self.emit_stmt(body)?;
8920                }
8921                self.current_handler_vars = old_handler_vars;
8922                self.go_declared_scopes.pop();
8923                self.indent -= 1;
8924                self.writeln("}");
8925                Ok(())
8926            }
8927            NodeKind::Assign { op, target, value } => {
8928                let ind = self.indent_str();
8929                let _ = write!(self.buf, "{ind}");
8930                self.emit_expr(target)?;
8931                let op_str = match op {
8932                    AssignOp::Assign => " = ",
8933                    AssignOp::AddAssign => " += ",
8934                    AssignOp::SubAssign => " -= ",
8935                    AssignOp::MulAssign => " *= ",
8936                    AssignOp::DivAssign => " /= ",
8937                    AssignOp::RemAssign => " %= ",
8938                };
8939                self.buf.push_str(op_str);
8940                self.emit_expr(value)?;
8941                self.buf.push('\n');
8942                Ok(())
8943            }
8944            _ => {
8945                self.write_indent();
8946                self.emit_expr(node)?;
8947                self.buf.push('\n');
8948                Ok(())
8949            }
8950        }
8951    }
8952
8953    /// Emit an if statement that continues after an `} else`.
8954    fn emit_if_continued(&mut self, node: &AIRNode) -> Result<(), CodegenError> {
8955        if let NodeKind::If {
8956            condition,
8957            then_block,
8958            else_block,
8959            ..
8960        } = &node.kind
8961        {
8962            let _ = write!(self.buf, "if ");
8963            self.emit_expr(condition)?;
8964            self.buf.push_str(" {\n");
8965            self.indent += 1;
8966            self.emit_block_body(then_block)?;
8967            self.indent -= 1;
8968            if let Some(else_b) = else_block {
8969                if matches!(else_b.kind, NodeKind::If { .. }) {
8970                    let ind = self.indent_str();
8971                    let _ = write!(self.buf, "{ind}}} else ");
8972                    return self.emit_if_continued(else_b);
8973                }
8974                self.writeln("} else {");
8975                self.indent += 1;
8976                self.emit_block_body(else_b)?;
8977                self.indent -= 1;
8978            }
8979            self.writeln("}");
8980        }
8981        Ok(())
8982    }
8983
8984    // ── Expressions ─────────────────────────────────────────────────────────
8985
8986    fn emit_expr(&mut self, node: &AIRNode) -> Result<(), CodegenError> {
8987        match &node.kind {
8988            NodeKind::Literal { lit } => {
8989                match lit {
8990                    Literal::Int(s) => self.buf.push_str(s),
8991                    Literal::Float(s) => self.buf.push_str(s),
8992                    Literal::Bool(b) => self.buf.push_str(if *b { "true" } else { "false" }),
8993                    Literal::Char(s) => {
8994                        self.buf.push('\'');
8995                        self.buf.push_str(s);
8996                        self.buf.push('\'');
8997                    }
8998                    Literal::String(s) => {
8999                        self.buf.push('"');
9000                        self.buf.push_str(&escape_go_string(s));
9001                        self.buf.push('"');
9002                    }
9003                    Literal::Unit => self.buf.push_str("nil"),
9004                }
9005                Ok(())
9006            }
9007            NodeKind::Identifier { name } => {
9008                if name.name == "None" {
9009                    self.buf.push_str("__bockNone");
9010                    return Ok(());
9011                }
9012                // Prelude `Ordering` variant → the bare `__bockOrdering` constant
9013                // (`Less`/`Equal`/`Greater`) of the Ordering runtime, which a
9014                // value-switch `case Less:` and the `compare` bridge also use —
9015                // UNLESS the real `core.compare.Ordering` enum is reachable, in
9016                // which case the reference is a user-enum variant-struct
9017                // construction (`OrderingLess{}`), handled by the path below.
9018                if crate::generator::ordering_variant(&name.name).is_some()
9019                    && !self.ordering_enum_reachable()
9020                {
9021                    self.buf.push_str(&name.name);
9022                    return Ok(());
9023                }
9024                // A unit-variant reference (`Empty`) → an empty variant-struct
9025                // literal `ShapeEmpty{}`. For a *generic* enum the variant struct
9026                // carries the enum's type params (`type BoxEmpty[T any] struct{}`),
9027                // so the literal must spell the concrete instantiation
9028                // (`BoxEmpty[int64]{}`) drawn from the binding's expected type —
9029                // Go rejects a bare generic struct literal.
9030                if let Some(enum_name) = self
9031                    .user_variant_for_name(&name.name)
9032                    .map(|i| i.enum_name.clone())
9033                {
9034                    let type_args = self.expected_enum_variant_type_arg_suffix(&enum_name);
9035                    let _ = write!(self.buf, "{enum_name}{}{type_args}{{}}", name.name);
9036                    return Ok(());
9037                }
9038                // A module-scope `const` is emitted verbatim at its declaration;
9039                // spell its use site identically (the `go_fn_name` transform would
9040                // camelCase a SCREAMING_SNAKE name, e.g. `FIZZ_NUM` → `fizzNUM`).
9041                if self.const_names.contains(&name.name) {
9042                    self.buf.push_str(&name.name);
9043                    return Ok(());
9044                }
9045                let emitted = if is_prelude_ctor(&name.name) {
9046                    name.name.clone()
9047                } else if self.local_shadows_public_fn(&name.name) {
9048                    // An in-scope local (param/`let`/bind) shadows a same-named
9049                    // public module fn — spell the *local*, not the PascalCased
9050                    // helper (Q-go-runtime-helper-shadowing).
9051                    go_value_ident(&name.name)
9052                } else {
9053                    // Routes a public name colliding with a type through the
9054                    // `Fn`-suffix rename (`key` → `KeyFn`); a private name is
9055                    // camelCased.
9056                    self.go_fn_name(&name.name)
9057                };
9058                self.buf.push_str(&emitted);
9059                Ok(())
9060            }
9061            NodeKind::BinaryOp { op, left, right } => {
9062                // `+` on two `List[T]` operands is concatenation. Go has no `+`
9063                // for slices (`operator + not defined on []T`), so build a fresh
9064                // slice with `append(append([]T{}, a...), b...)`. The element type
9065                // comes from a list-typed operand or the binding's expected type.
9066                if matches!(op, BinOp::Add) && crate::generator::is_list_concat(node, left, right) {
9067                    let elem = self
9068                        .list_receiver_elem_go_type(left)
9069                        .or_else(|| self.list_receiver_elem_go_type(right))
9070                        .or_else(|| {
9071                            self.current_expected_type
9072                                .as_deref()
9073                                .and_then(|t| t.strip_prefix("[]"))
9074                                .map(str::to_string)
9075                        })
9076                        .unwrap_or_else(|| "interface{}".to_string());
9077                    // A list-literal operand must adopt `elem` as its element type
9078                    // — a `[]interface{}{x}` literal is not assignable to the
9079                    // `[]elem` slice `append` builds. Thread the expected element
9080                    // into each literal operand (mirrors the `concat` method).
9081                    let emit_operand =
9082                        |this: &mut Self, n: &AIRNode| -> Result<String, CodegenError> {
9083                            let prev = this.expected_collection_elem.take();
9084                            if matches!(n.kind, NodeKind::ListLiteral { .. }) {
9085                                this.expected_collection_elem = Some((elem.clone(), None));
9086                            }
9087                            let s = this.expr_to_string(n);
9088                            this.expected_collection_elem = prev;
9089                            s
9090                        };
9091                    let l = emit_operand(self, left)?;
9092                    let r = emit_operand(self, right)?;
9093                    let _ = write!(self.buf, "append(append([]{elem}{{}}, {l}...), {r}...)");
9094                    return Ok(());
9095                }
9096                // `a ** b`: Go has no `**`. A *float* power lowers to `math.Pow`
9097                // (which takes and returns `float64`); an *integer* power lowers
9098                // to the `__bockIntPow` runtime helper (stays in `int64`, exact).
9099                // Operands are coerced to the chosen numeric type so a mixed
9100                // `2 ** f` (Int literal ** Float) still type-checks.
9101                if matches!(op, BinOp::Pow) {
9102                    let l = self.expr_to_string(left)?;
9103                    let r = self.expr_to_string(right)?;
9104                    if self.pow_is_float(left, right) {
9105                        self.needs_math_import = true;
9106                        let _ = write!(self.buf, "math.Pow(float64({l}), float64({r}))");
9107                    } else {
9108                        let _ = write!(self.buf, "__bockIntPow(int64({l}), int64({r}))");
9109                    }
9110                    return Ok(());
9111                }
9112                // Ordering operators on a user `Comparable` type lower through the
9113                // type's `Compare` (Go structs are not ordered, so native `<` is a
9114                // compile error). `Compare` returns the sealed `Ordering` interface;
9115                // a type assertion against the variant struct yields the boolean,
9116                // wrapped in an IIFE (Go's comma-ok assertion is statement-only):
9117                // `a < b` ⇒ `… .(OrderingLess); return __ok`, `a <= b` ⇒
9118                // `… .(OrderingGreater); return !__ok`, etc.
9119                if crate::generator::is_user_compare(node) {
9120                    if let Some((tag, is_eq)) = crate::generator::user_compare_variant(*op) {
9121                        let recv = self.expr_to_string(left)?;
9122                        let other = self.expr_to_string(right)?;
9123                        let method = self.go_method_name("compare", true);
9124                        let neg = if is_eq { "" } else { "!" };
9125                        let _ = write!(
9126                            self.buf,
9127                            "func() bool {{ _, __ok := ({recv}.{method}({other})).(Ordering{tag}); return {neg}__ok }}()"
9128                        );
9129                        return Ok(());
9130                    }
9131                }
9132                // DQ29 (§18.5 structural Equatable): a stamped `==`/`!=` whose
9133                // operand has an explicit `impl Equatable` dispatches through
9134                // its `Eq` method (Go's native struct `==` is field-wise and
9135                // would silently ignore the user's custom equality). The
9136                // `"deep"` lane — operands involving a `List`/`Map`/`Set`
9137                // (no native `==`: "slice/map can only be compared to nil") —
9138                // lowers through the `__bockDeepEq` runtime helper, whose
9139                // `reflect.DeepEqual` is element-wise for slices and
9140                // order-independent for maps (Bock `Map`/`Set`). The
9141                // `"deep_custom"` lane (DQ31, §18.5) — a container whose element
9142                // tree carries an explicit `impl Equatable` — routes through
9143                // `__bockEqCustom`, which dispatches element comparison and
9144                // Map-key / Set-member matching through the element's `Eq`. The
9145                // `"structural"` and `"generic"` lanes stay native: Go struct/
9146                // interface equality is already field-wise (tag-then-payload
9147                // for the enum interface form), and a `comparable`-constrained
9148                // type param compares natively.
9149                if matches!(op, BinOp::Eq | BinOp::Ne) {
9150                    match crate::generator::user_eq_kind(node) {
9151                        Some("impl") => {
9152                            let recv = self.expr_to_string(left)?;
9153                            let other = self.expr_to_string(right)?;
9154                            let method = self.go_method_name("eq", true);
9155                            let neg = if *op == BinOp::Ne { "!" } else { "" };
9156                            let _ = write!(self.buf, "{neg}({recv}).{method}({other})");
9157                            return Ok(());
9158                        }
9159                        Some("deep") => {
9160                            let recv = self.expr_to_string(left)?;
9161                            let other = self.expr_to_string(right)?;
9162                            let neg = if *op == BinOp::Ne { "!" } else { "" };
9163                            let _ = write!(self.buf, "{neg}__bockDeepEq({recv}, {other})");
9164                            return Ok(());
9165                        }
9166                        // DQ31: a container whose element tree carries a custom
9167                        // `impl Equatable` routes through `__bockEqCustom`,
9168                        // which dispatches element comparison (and Map-key /
9169                        // Set-member matching) through the element's `Eq`
9170                        // method rather than `reflect.DeepEqual`.
9171                        Some("deep_custom") => {
9172                            let recv = self.expr_to_string(left)?;
9173                            let other = self.expr_to_string(right)?;
9174                            let neg = if *op == BinOp::Ne { "!" } else { "" };
9175                            let _ = write!(self.buf, "{neg}__bockEqCustom({recv}, {other})");
9176                            return Ok(());
9177                        }
9178                        _ => {}
9179                    }
9180                }
9181                self.buf.push('(');
9182                self.emit_expr(left)?;
9183                let op_str = match op {
9184                    BinOp::Add => " + ",
9185                    BinOp::Sub => " - ",
9186                    BinOp::Mul => " * ",
9187                    BinOp::Div => " / ",
9188                    BinOp::Rem => " % ",
9189                    // `Pow` is lowered above (math.Pow / __bockIntPow) and never
9190                    // reaches this arm; kept for match exhaustiveness.
9191                    BinOp::Pow => " /* pow */ ",
9192                    BinOp::Eq => " == ",
9193                    BinOp::Ne => " != ",
9194                    BinOp::Lt => " < ",
9195                    BinOp::Le => " <= ",
9196                    BinOp::Gt => " > ",
9197                    BinOp::Ge => " >= ",
9198                    BinOp::And => " && ",
9199                    BinOp::Or => " || ",
9200                    BinOp::BitAnd => " & ",
9201                    BinOp::BitOr => " | ",
9202                    BinOp::BitXor => " ^ ",
9203                    BinOp::Compose => " /* compose */ ",
9204                    BinOp::Is => " == ",
9205                };
9206                self.buf.push_str(op_str);
9207                self.emit_expr(right)?;
9208                self.buf.push(')');
9209                Ok(())
9210            }
9211            NodeKind::UnaryOp { op, operand } => {
9212                let op_str = match op {
9213                    UnaryOp::Neg => "-",
9214                    UnaryOp::Not => "!",
9215                    UnaryOp::BitNot => "^",
9216                };
9217                self.buf.push_str(op_str);
9218                self.emit_expr(operand)?;
9219                Ok(())
9220            }
9221            NodeKind::Call {
9222                callee,
9223                args,
9224                type_args,
9225            } => {
9226                // Effect operation Call → handler.Op rewriting.
9227                if let NodeKind::Identifier { name } = &callee.kind {
9228                    if let Some(effect_name) = self.effect_ops.get(&name.name).cloned() {
9229                        if let Some(handler_var) =
9230                            self.current_handler_vars.get(&effect_name).cloned()
9231                        {
9232                            let _ =
9233                                write!(self.buf, "{}.{}", handler_var, to_pascal_case(&name.name));
9234                            self.buf.push('(');
9235                            for (i, arg) in args.iter().enumerate() {
9236                                if i > 0 {
9237                                    self.buf.push_str(", ");
9238                                }
9239                                self.emit_expr(&arg.value)?;
9240                            }
9241                            self.buf.push(')');
9242                            return Ok(());
9243                        }
9244                    }
9245                }
9246                if let Some(code) = self.map_prelude_call(callee, args)? {
9247                    self.buf.push_str(&code);
9248                    return Ok(());
9249                }
9250                // A call whose callee names a registered tuple variant is a
9251                // construction → the variant-struct literal
9252                // `ShapeRect{Field0: 3.0, Field1: 4.0}`. For a *generic* enum the
9253                // variant struct carries the enum's type params
9254                // (`type BoxFull[T any] struct{…}`), so the literal must spell the
9255                // concrete instantiation (`BoxFull[int64]{…}`) from the binding's
9256                // expected type — Go does not infer struct type args from the
9257                // field values, and rejects a bare generic struct literal.
9258                if let NodeKind::Identifier { name } = &callee.kind {
9259                    if let Some(enum_name) = self
9260                        .user_variant_for_name(&name.name)
9261                        .map(|i| i.enum_name.clone())
9262                    {
9263                        let type_args = self.expected_enum_variant_type_arg_suffix(&enum_name);
9264                        let _ = write!(self.buf, "{enum_name}{}{type_args}{{", name.name);
9265                        for (i, arg) in args.iter().enumerate() {
9266                            if i > 0 {
9267                                self.buf.push_str(", ");
9268                            }
9269                            let _ = write!(self.buf, "Field{i}: ");
9270                            self.emit_expr(&arg.value)?;
9271                        }
9272                        self.buf.push('}');
9273                        return Ok(());
9274                    }
9275                }
9276                if self.try_emit_time_assoc_call(callee, args)? {
9277                    return Ok(());
9278                }
9279                if self.try_emit_time_desugared_method(node, callee, args)? {
9280                    return Ok(());
9281                }
9282                if self.try_emit_concurrency_call(callee, args)? {
9283                    return Ok(());
9284                }
9285                // Map/Set dispatch precedes the List recogniser so the
9286                // overlapping method names route by `recv_kind`, not by name.
9287                if self.try_emit_map_method(node, callee, args)? {
9288                    return Ok(());
9289                }
9290                if self.try_emit_set_method(node, callee, args)? {
9291                    return Ok(());
9292                }
9293                // String method dispatch runs *before* the List recogniser so the
9294                // overlapping `len`/`contains`/`is_empty` names route by the
9295                // checker's `recv_kind = "Primitive:String"`, not by name alone —
9296                // the fix for `String.contains` being misrouted to the List scan.
9297                if self.try_emit_string_method(node, callee, args)? {
9298                    return Ok(());
9299                }
9300                // Numeric/Char/Bool primitive methods (`to_float`/`abs`/`sqrt`/…)
9301                // likewise route by the checker's `recv_kind = "Primitive:Int|…"`
9302                // before the generic fall-through, which would emit `n.toFloat(n)`.
9303                if self.try_emit_numeric_method(node, callee, args)? {
9304                    return Ok(());
9305                }
9306                if self.try_emit_list_method(node, callee, args)? {
9307                    return Ok(());
9308                }
9309                if self.try_emit_list_inplace_mutator(node, callee, args)? {
9310                    return Ok(());
9311                }
9312                if self.try_emit_list_functional_method(node, callee, args)? {
9313                    return Ok(());
9314                }
9315                if self.try_emit_primitive_bridge(node, callee, args)? {
9316                    return Ok(());
9317                }
9318                if self.try_emit_trait_bound_bridge(node, callee, args)? {
9319                    return Ok(());
9320                }
9321                if self.try_emit_container_method(node, callee, args)? {
9322                    return Ok(());
9323                }
9324                // Q-prim-assoc: a primitive associated-conversion call
9325                // (`Float.from(x)` / `Int.try_from(s)` / `String.from(c)`)
9326                // lowers to Go's native conversion, NOT the free-function form
9327                // below (`Float_from` is undefined).
9328                if self.try_emit_primitive_conversion(node, callee, args)? {
9329                    return Ok(());
9330                }
9331                // Associated-function call (`Type.method(args)` — stamped by the
9332                // lowerer, no `self` prepended). Go has no static methods, so the
9333                // definition is a free function `Type_Method(...)`
9334                // (`emit_associated_fn`); emit the matching free-function call.
9335                // The `public_methods` check picks the same Pascal/camel casing
9336                // the definition used (trait-impl associated fns are always
9337                // exported).
9338                if crate::generator::is_associated_call(node) {
9339                    if let NodeKind::FieldAccess { object, field } = &callee.kind {
9340                        if let NodeKind::Identifier { name: type_name } = &object.kind {
9341                            let is_public = self.public_methods.contains(&field.name);
9342                            let fn_name =
9343                                self.freefn_lowered_name(&type_name.name, &field.name, is_public);
9344                            let _ = write!(self.buf, "{fn_name}(");
9345                            for (i, arg) in args.iter().enumerate() {
9346                                if i > 0 {
9347                                    self.buf.push_str(", ");
9348                                }
9349                                self.emit_expr(&arg.value)?;
9350                            }
9351                            self.buf.push(')');
9352                            return Ok(());
9353                        }
9354                    }
9355                }
9356                // Desugared instance method call `Call(FieldAccess(recv, m),
9357                // [recv, ...rest])`: emit `recv.M(rest)` using Go method casing
9358                // so the receiver flows through the native `self` receiver
9359                // rather than as a duplicated `interface{}` argument.
9360                if let Some((recv, method, rest)) =
9361                    crate::generator::desugared_self_call(callee, args)
9362                {
9363                    // DQ28 free-function lowering: a generic method
9364                    // (`box.map(f)`) lowers to a free function call
9365                    // `Box_Map(box, f)` — the receiver leads as the first
9366                    // argument. The method name uniquely identifies the type
9367                    // (poisoned otherwise), so the rewrite is unambiguous.
9368                    if let Some(ty) = self.freefn_lowered_type(&method.name).map(str::to_string) {
9369                        let is_public = self.public_methods.contains(&method.name);
9370                        let fn_name = self.freefn_lowered_name(&ty, &method.name, is_public);
9371                        let _ = write!(self.buf, "{fn_name}(");
9372                        self.emit_expr(recv)?;
9373                        for arg in rest {
9374                            self.buf.push_str(", ");
9375                            self.emit_expr(&arg.value)?;
9376                        }
9377                        self.buf.push(')');
9378                        return Ok(());
9379                    }
9380                    self.emit_expr(recv)?;
9381                    let go_method = self
9382                        .go_method_name(&method.name, self.public_methods.contains(&method.name));
9383                    let _ = write!(self.buf, ".{go_method}(");
9384                    for (i, arg) in rest.iter().enumerate() {
9385                        if i > 0 {
9386                            self.buf.push_str(", ");
9387                        }
9388                        self.emit_expr(&arg.value)?;
9389                    }
9390                    self.buf.push(')');
9391                    return Ok(());
9392                }
9393                // Pass handler args to effectful function calls.
9394                let effects_args = if let NodeKind::Identifier { name } = &callee.kind {
9395                    self.build_effects_call_args_go(&name.name)
9396                } else {
9397                    None
9398                };
9399                // Route async-fn calls through their `Async`-suffix wrapper
9400                // so callers receive a `<-chan T` instead of `T` — the sync
9401                // body is only invoked from inside its own wrapper.
9402                if let NodeKind::Identifier { name } = &callee.kind {
9403                    if self.async_fns.contains(&name.name) {
9404                        let go_name = self.go_fn_name(&name.name);
9405                        self.buf.push_str(&format!("{go_name}Async"));
9406                    } else {
9407                        self.emit_expr(callee)?;
9408                    }
9409                } else {
9410                    self.emit_expr(callee)?;
9411                }
9412                // When the callee is a known generic fn, recover its signature so
9413                // we can (a) synthesise explicit Go type-arguments the source
9414                // omits but Go cannot infer (a `Optional[T]`/`Result[T, E]` param
9415                // erases `T`/`E` from the monomorphic runtime struct), and (b)
9416                // specialise each untyped lambda argument to the concrete Go param
9417                // type (`func(int64) bool` for `filter(it, (x) => x > 2)`).
9418                let fn_sig = if let NodeKind::Identifier { name } = &callee.kind {
9419                    self.fn_signatures.get(&name.name).cloned()
9420                } else {
9421                    None
9422                };
9423                // Synthesise the turbofish only when the source carried none.
9424                // An explicit source `f[Ty](..)` (`type_args`) always wins.
9425                let callee_sealed_bound = matches!(&callee.kind, NodeKind::Identifier { name }
9426                    if self.fn_sealed_bound.contains(&name.name));
9427                let synthesized_type_args = if type_args.is_empty() {
9428                    fn_sig.as_ref().and_then(|(gp, ptys, ret)| {
9429                        // A container-touching signature defeats Go's own inference
9430                        // (the `Optional`/`Result` runtime erases `T`); a
9431                        // sealed-core-bound fn defeats untyped-constant inference
9432                        // (GAP-C). Either forces explicit type args.
9433                        self.synthesize_go_type_args(
9434                            gp,
9435                            ptys,
9436                            ret.as_ref(),
9437                            args,
9438                            callee_sealed_bound,
9439                        )
9440                    })
9441                } else {
9442                    None
9443                };
9444                let type_arg_str = if let Some(syn) = &synthesized_type_args {
9445                    format!("[{}]", syn.join(", "))
9446                } else {
9447                    self.format_generic_args(type_args)
9448                };
9449                self.buf.push_str(&type_arg_str);
9450                self.buf.push('(');
9451                // Bind the callee's type params from the (synthesised args, when
9452                // available, else the non-lambda arguments) so each untyped lambda
9453                // argument can be specialised to the concrete Go param type rather
9454                // than the `interface{}` default that breaks the body and
9455                // mismatches the typed callee param. The synthesised binding is
9456                // strictly more complete than the arg-only one — it also pins a
9457                // `T` that only appears behind `Optional[T]`/`Result[T, E]`, which
9458                // is exactly the `filter`/`and_then` lambda case.
9459                let lambda_bindings = fn_sig
9460                    .as_ref()
9461                    .map(|(gp, ptys, _)| {
9462                        let binds = match (&synthesized_type_args, gp.len()) {
9463                            (Some(syn), n) if syn.len() == n => {
9464                                gp.iter().cloned().zip(syn.iter().cloned()).collect()
9465                            }
9466                            _ => self.bind_fn_type_params(gp, ptys, args),
9467                        };
9468                        (gp.clone(), ptys.clone(), binds)
9469                    })
9470                    .or_else(|| {
9471                        // Non-generic callee: no type params to bind, but its
9472                        // concrete `Fn(...)` param types still pin an untyped lambda
9473                        // argument (`count_where(todos, (t) => t.done)` →
9474                        // `func(t Todo) bool`). Empty gp/bindings means
9475                        // `specialise_lambda_param_types` renders the param types
9476                        // verbatim.
9477                        if let NodeKind::Identifier { name } = &callee.kind {
9478                            self.fn_param_types
9479                                .get(&name.name)
9480                                .map(|ptys| (Vec::new(), ptys.clone(), HashMap::new()))
9481                        } else {
9482                            None
9483                        }
9484                    });
9485                for (i, arg) in args.iter().enumerate() {
9486                    if i > 0 {
9487                        self.buf.push_str(", ");
9488                    }
9489                    let prev_lambda = self.expected_lambda_param_types.take();
9490                    let prev_forced_ret = self.forced_lambda_ret.take();
9491                    let prev_coll = self.expected_collection_elem.take();
9492                    if matches!(arg.value.kind, NodeKind::Lambda { .. }) {
9493                        if let Some((gp, ptys, binds)) = &lambda_bindings {
9494                            if let Some(pty) = ptys.get(i).and_then(|p| p.as_ref()) {
9495                                self.expected_lambda_param_types =
9496                                    self.specialise_lambda_param_types(pty, gp, binds);
9497                                // A `Fn(...) -> Void` callee parameter pins the
9498                                // lambda's return to the Void marker so the closure
9499                                // emits `func(...) { <stmts> }` (no result, no
9500                                // `return`). Without this, a `() => println(...)`
9501                                // call argument would emit `func() interface{} {
9502                                // return fmt.Println(...) }` — both a type mismatch
9503                                // against the `func()` parameter and a Go arity
9504                                // error (`fmt.Println` returns `(int, error)`). The
9505                                // parameter may name a `type Handler = Fn() -> Void`
9506                                // alias (react-components' `EventHandler`), so peel
9507                                // one alias layer first.
9508                                let resolved = self.resolve_type_alias(pty).unwrap_or(pty);
9509                                if let NodeKind::TypeFunction { ret, .. } = &resolved.kind {
9510                                    if Self::is_void_type(ret) {
9511                                        self.forced_lambda_ret = Some("struct{}".to_string());
9512                                    }
9513                                }
9514                            }
9515                        }
9516                    } else if matches!(
9517                        arg.value.kind,
9518                        NodeKind::ListLiteral { .. }
9519                            | NodeKind::MapLiteral { .. }
9520                            | NodeKind::SetLiteral { .. }
9521                    ) {
9522                        // A collection literal argument (most importantly an empty
9523                        // `[]` / `{}` whose own elements can't infer a type) adopts
9524                        // the callee's declared parameter element type, so it emits
9525                        // `[]int64{}` against a `List[Int]` param rather than the
9526                        // erased `[]interface{}{}` Go rejects. The param type comes
9527                        // from the non-generic `fn_param_types` record.
9528                        if let NodeKind::Identifier { name } = &callee.kind {
9529                            if let Some(pty) = self
9530                                .fn_param_types
9531                                .get(&name.name)
9532                                .and_then(|ptys| ptys.get(i))
9533                                .and_then(|p| p.as_ref())
9534                            {
9535                                self.expected_collection_elem = self.collection_elem_go_types(pty);
9536                            }
9537                        }
9538                    }
9539                    self.emit_expr(&arg.value)?;
9540                    self.expected_lambda_param_types = prev_lambda;
9541                    self.forced_lambda_ret = prev_forced_ret;
9542                    self.expected_collection_elem = prev_coll;
9543                }
9544                if let Some(ea) = effects_args {
9545                    if !args.is_empty() {
9546                        self.buf.push_str(", ");
9547                    }
9548                    self.buf.push_str(&ea);
9549                }
9550                self.buf.push(')');
9551                Ok(())
9552            }
9553            NodeKind::MethodCall {
9554                receiver,
9555                method,
9556                args,
9557                ..
9558            } => {
9559                if self.try_emit_time_method(receiver, &method.name, args)? {
9560                    return Ok(());
9561                }
9562                // DQ28 free-function lowering (the non-desugared `MethodCall`
9563                // shape): `box.map(f)` → `Box_Map(box, f)`, receiver-first.
9564                if let Some(ty) = self.freefn_lowered_type(&method.name).map(str::to_string) {
9565                    let is_public = self.public_methods.contains(&method.name);
9566                    let fn_name = self.freefn_lowered_name(&ty, &method.name, is_public);
9567                    let _ = write!(self.buf, "{fn_name}(");
9568                    self.emit_expr(receiver)?;
9569                    for arg in args {
9570                        self.buf.push_str(", ");
9571                        self.emit_expr(&arg.value)?;
9572                    }
9573                    self.buf.push(')');
9574                    return Ok(());
9575                }
9576                self.emit_expr(receiver)?;
9577                // `MethodCall` dispatches a method through Go method casing. A
9578                // method whose name collides with a struct field is suffixed
9579                // identically here and at the declaration (`go_method_name`).
9580                let go_method =
9581                    self.go_method_name(&method.name, self.public_methods.contains(&method.name));
9582                let _ = write!(self.buf, ".{go_method}");
9583                self.buf.push('(');
9584                for (i, arg) in args.iter().enumerate() {
9585                    if i > 0 {
9586                        self.buf.push_str(", ");
9587                    }
9588                    self.emit_expr(&arg.value)?;
9589                }
9590                self.buf.push(')');
9591                Ok(())
9592            }
9593            NodeKind::FieldAccess { object, field } => {
9594                self.emit_expr(object)?;
9595                let _ = write!(self.buf, ".{}", to_pascal_case(&field.name));
9596                Ok(())
9597            }
9598            NodeKind::Index { object, index } => {
9599                self.emit_expr(object)?;
9600                self.buf.push('[');
9601                self.emit_expr(index)?;
9602                self.buf.push(']');
9603                Ok(())
9604            }
9605            NodeKind::Lambda { params, body } => {
9606                // An untyped lambda argument adopts the callee's specialised
9607                // parameter types when known (`expected_lambda_param_types`,
9608                // e.g. `func(int64) bool` for `filter(it, (x) => x > 2)`), so
9609                // the body's arithmetic type-checks and the closure satisfies
9610                // the typed callee parameter. Consume the hint so it never
9611                // leaks to a nested lambda in the body.
9612                let expected_params = self.expected_lambda_param_types.take();
9613                // A `f >> g` compose desugars (in shared AIR) to `(__compose_x) =>
9614                // g(f(__compose_x))` with an *untyped* `__compose_x`. Recover its
9615                // Go type from `f`'s first declared parameter type so the emitted
9616                // closure is `func(x []float64) ...` rather than `func(x
9617                // interface{}) ...` — the latter is not assignable to a typed
9618                // `Fn(List[Float]) -> List[Float]` callee parameter.
9619                let compose_param = if expected_params.is_none() {
9620                    self.compose_lambda_param_go_type(params, body)
9621                } else {
9622                    None
9623                };
9624                let param_strs = match (&expected_params, &compose_param) {
9625                    (Some(tys), _) if tys.len() == params.len() => {
9626                        self.collect_param_strs_with_types(params, tys)
9627                    }
9628                    (None, Some(ty)) => {
9629                        self.collect_param_strs_with_types(params, std::slice::from_ref(ty))
9630                    }
9631                    _ => self.collect_param_strs(params),
9632                };
9633                // Record the lambda's typed params so the body's return type can
9634                // be inferred structurally. Without a concrete return type Go
9635                // infers `interface{}`, which fails to satisfy a typed
9636                // `func(int64) int64` parameter at the use site.
9637                let scope_expected = expected_params
9638                    .as_deref()
9639                    .or(compose_param.as_ref().map(std::slice::from_ref));
9640                let saved_go_types =
9641                    self.enter_param_go_types_with_expected(params, scope_expected);
9642                // A predicate combinator pins the return type to `bool` (consumed
9643                // here so it never leaks to a nested lambda); otherwise infer it.
9644                // Use the block-tail inference so a lambda whose body is a block /
9645                // `if` / `match` (`(t) => { if (..) { t.complete() } else { t } }`)
9646                // gets a concrete return type (`Todo`) rather than `interface{}` —
9647                // this must agree with the result-slice element type the
9648                // list-combinator emitter derives from the same inference, or
9649                // `append(__out, __f(__x))` mismatches `[]Todo` vs `interface{}`.
9650                let forced_ret = self.forced_lambda_ret.take();
9651                // A `Fn(...) -> Void` callee pins `forced_ret` to the Void value
9652                // type `struct{}` (and the *type* now renders `func(...)` with no
9653                // result, see `type_to_go`'s `TypeFunction` arm). Such a lambda is
9654                // void: its closure must be `func(...) { <stmts> }` — no result
9655                // type, no `return`. Emitting `func() struct{} { return <body> }`
9656                // is doubly wrong: it does not match the `func()` parameter type,
9657                // and a void-call body (`println(...)` → Go's `fmt.Println`, which
9658                // returns `(int, error)`) makes `return fmt.Println(...)` a Go
9659                // arity error. When `forced_ret` is unset, a lambda whose body tail
9660                // is a void call (a bare `() => println(...)`) is likewise void.
9661                let body_tail = match &body.kind {
9662                    NodeKind::Block { tail: Some(t), .. } => t.as_ref(),
9663                    NodeKind::Block { tail: None, .. } => body,
9664                    _ => body,
9665                };
9666                let is_void_lambda = forced_ret.as_deref() == Some("struct{}")
9667                    || (forced_ret.is_none()
9668                        && expected_params.is_none()
9669                        && compose_param.is_none()
9670                        && self.is_void_call(body_tail));
9671                if is_void_lambda {
9672                    // Statement-style void closure: `func(params) { <stmts> }`. The
9673                    // body's effective tail void call is emitted as a statement
9674                    // (never `return`d). Mirrors the function-body void path.
9675                    let _ = write!(self.buf, "func({}) {{ ", param_strs.join(", "));
9676                    let prev_ret = self.current_fn_ret_type.take();
9677                    let prev_expected = self.current_expected_type.take();
9678                    if let NodeKind::Block { stmts, .. } = &body.kind {
9679                        for s in stmts {
9680                            self.emit_node(s)?;
9681                            self.buf.push_str("; ");
9682                        }
9683                    }
9684                    self.emit_expr(body_tail)?;
9685                    self.current_fn_ret_type = prev_ret;
9686                    self.current_expected_type = prev_expected;
9687                    self.buf.push_str(" }");
9688                    self.var_go_type = saved_go_types;
9689                    return Ok(());
9690                }
9691                let ret_ty = forced_ret.unwrap_or_else(|| {
9692                    self.infer_block_tail_type(body)
9693                        .unwrap_or_else(|| "interface{}".to_string())
9694                });
9695                let _ = write!(
9696                    self.buf,
9697                    "func({}) {ret_ty} {{ return ",
9698                    param_strs.join(", ")
9699                );
9700                // The lambda body is a fresh return scope: a `match`/`if`/`loop`
9701                // in its tail lowers to an IIFE whose type is *this lambda's*
9702                // return type, not the enclosing function's. Without resetting
9703                // these, an inner match IIFE in a `filter`/`map` lambda inherited
9704                // the outer fn's return type (chat-protocol: a `bool` match body
9705                // typed `func() []Message`, the surrounding `filter`'s return).
9706                let prev_ret = self.current_fn_ret_type.take();
9707                let prev_expected = self.current_expected_type.take();
9708                self.current_fn_ret_type = (ret_ty != "interface{}").then(|| ret_ty.clone());
9709                self.emit_expr(body)?;
9710                self.current_fn_ret_type = prev_ret;
9711                self.current_expected_type = prev_expected;
9712                self.buf.push_str(" }");
9713                self.var_go_type = saved_go_types;
9714                Ok(())
9715            }
9716            NodeKind::Pipe { left, right } => self.emit_pipe(left, right),
9717            NodeKind::Compose { left, right } => {
9718                // `f >> g` → `func(x interface{}) interface{} { return g(f(x)) }`
9719                let _ = write!(self.buf, "func(x interface{{}}) interface{{}} {{ return ");
9720                self.emit_expr(right)?;
9721                self.buf.push('(');
9722                self.emit_expr(left)?;
9723                self.buf.push_str("(x)) }");
9724                Ok(())
9725            }
9726            NodeKind::Await { expr } => {
9727                // Go uses goroutines/channels; await maps to channel receive.
9728                self.buf.push_str("<-");
9729                self.emit_expr(expr)?;
9730                Ok(())
9731            }
9732            NodeKind::Propagate { expr } => {
9733                // Go error propagation would require special handling;
9734                // just emit the expression for now.
9735                self.emit_expr(expr)?;
9736                Ok(())
9737            }
9738            NodeKind::Range { lo, hi, inclusive } => {
9739                // Go has no native range *value*; lower to the injected
9740                // `__bockRange(lo, hi, inclusive)` helper (a `[]int64`), so
9741                // `for _, i := range <range>` iterates the materialised slice.
9742                // The runtime is emitted once at the Module arm
9743                // (`go_module_uses_range`).
9744                self.buf.push_str("__bockRange(");
9745                self.emit_expr(lo)?;
9746                self.buf.push_str(", ");
9747                self.emit_expr(hi)?;
9748                let _ = write!(self.buf, ", {inclusive})");
9749                Ok(())
9750            }
9751            NodeKind::RecordConstruct {
9752                path,
9753                fields,
9754                spread,
9755            } => {
9756                // A struct-variant construction (`Circle { radius: .. }`) → the
9757                // `{enum}{variant}` struct literal `ShapeCircle{Radius: ..}`
9758                // (field name `to_pascal_case`d). Plain records keep their path.
9759                let type_name = self.record_construct_go_type_name(path);
9760                // Go requires an explicit type-argument list on a generic
9761                // struct literal (`Box[int64]{...}`); it does NOT infer the args
9762                // from the field values. Prefer the declared/expected binding
9763                // type's concrete args (`let c: ListIter[Int] = ListIter { ... }`
9764                // → `[int64]`), which works even when a param appears only
9765                // *nested* in a field type (`record ListIter[T] { xs: List[T] }`,
9766                // where no field is typed exactly `T` so field-inference yields
9767                // `any`). Fall back to inferring each param's type from the field
9768                // value that names it directly.
9769                let type_args = self
9770                    .expected_construct_type_args(&type_name)
9771                    .unwrap_or_else(|| self.infer_construct_type_args(&type_name, fields));
9772                if let Some(sp) = spread {
9773                    // Go has no struct-spread syntax (`Point{..p}`), so a record
9774                    // spread lowers to an IIFE that copies the base value, then
9775                    // assigns each override field, then returns the copy:
9776                    //   func() T { __s := <base>; __s.Field = val; …; return __s }()
9777                    // The base is the spread expression; the overrides are the
9778                    // explicitly-given fields. (A struct copy in Go is a value
9779                    // copy, so this does not mutate the base.)
9780                    let _ = write!(self.buf, "func() {type_name}{type_args} {{ __s := ");
9781                    self.emit_expr(sp)?;
9782                    self.buf.push_str("; ");
9783                    for f in fields {
9784                        let _ = write!(self.buf, "__s.{} = ", to_pascal_case(&f.name.name));
9785                        if let Some(val) = &f.value {
9786                            self.emit_expr(val)?;
9787                        } else {
9788                            self.buf.push_str(&to_camel_case(&f.name.name));
9789                        }
9790                        self.buf.push_str("; ");
9791                    }
9792                    self.buf.push_str("return __s }()");
9793                } else {
9794                    // A field whose declared type is `List[..]` (registered in
9795                    // `record_field_list_elem`) supplies the expected element type
9796                    // for a list-literal value, so an empty `[]` / under-inferred
9797                    // literal field emits `[]<elem>{}` matching the struct's `[]T`
9798                    // field rather than the erased `[]interface{}{}` Go rejects.
9799                    // The element type is the field's declared param (`T`),
9800                    // substituted with the construct's resolved concrete args
9801                    // (`SortedSet[Key]` → `T` ↦ `Key`).
9802                    let record_name = path.segments.last().map(|s| s.name.clone());
9803                    let param_subst = record_name
9804                        .as_deref()
9805                        .map(|rn| self.record_param_substitution(rn, &type_args))
9806                        .unwrap_or_default();
9807                    self.buf.push_str(&format!("{type_name}{type_args}{{"));
9808                    for (i, f) in fields.iter().enumerate() {
9809                        if i > 0 {
9810                            self.buf.push_str(", ");
9811                        }
9812                        let _ = write!(self.buf, "{}: ", to_pascal_case(&f.name.name));
9813                        if let Some(val) = &f.value {
9814                            let field_elem = record_name.as_deref().and_then(|rn| {
9815                                self.record_field_list_elem
9816                                    .get(rn)
9817                                    .and_then(|m| m.get(&f.name.name))
9818                                    .map(|e| Self::apply_type_subst(e, &param_subst))
9819                            });
9820                            let prev_expected = self.expected_collection_elem.take();
9821                            if let (Some(elem), true) = (
9822                                field_elem,
9823                                matches!(
9824                                    val.kind,
9825                                    NodeKind::ListLiteral { .. } | NodeKind::SetLiteral { .. }
9826                                ),
9827                            ) {
9828                                self.expected_collection_elem = Some((elem, None));
9829                            }
9830                            self.emit_expr(val)?;
9831                            self.expected_collection_elem = prev_expected;
9832                        } else {
9833                            self.buf.push_str(&to_camel_case(&f.name.name));
9834                        }
9835                    }
9836                    self.buf.push('}');
9837                }
9838                Ok(())
9839            }
9840            NodeKind::ListLiteral { elems } => {
9841                // A declared binding type (`let x: List[T] = ...`) takes priority
9842                // so an empty `[]` matches the declared `[]T`; otherwise infer a
9843                // homogeneous element type so `[1, 2, 3]` emits `[]int64{...}`
9844                // (not `[]interface{}{...}`), letting element arithmetic / typed
9845                // iteration / typed returns compile. Falls back to `interface{}`
9846                // when neither is available (empty literal with no declared
9847                // type, mixed types, unresolved operands).
9848                let expected = self.expected_collection_elem.take();
9849                let elem_ty = expected
9850                    .map(|(e, _)| e)
9851                    .or_else(|| self.infer_homogeneous_elem_type(elems))
9852                    .unwrap_or_else(|| "interface{}".to_string());
9853                let _ = write!(self.buf, "[]{elem_ty}{{");
9854                for (i, e) in elems.iter().enumerate() {
9855                    if i > 0 {
9856                        self.buf.push_str(", ");
9857                    }
9858                    self.emit_expr(e)?;
9859                }
9860                self.buf.push('}');
9861                Ok(())
9862            }
9863            NodeKind::MapLiteral { entries } => {
9864                // A declared `Map[K, V]` binding type takes priority (so an
9865                // empty `{}` matches `map[K]V`); otherwise infer key and value
9866                // element types separately so `{"a": 1}` emits
9867                // `map[string]int64{...}`. Either falling back to `interface{}`.
9868                let expected = self.expected_collection_elem.take();
9869                let keys: Vec<&AIRNode> = entries.iter().map(|e| &e.key).collect();
9870                let vals: Vec<&AIRNode> = entries.iter().map(|e| &e.value).collect();
9871                let (exp_key, exp_val) = match expected {
9872                    Some((k, v)) => (Some(k), v),
9873                    None => (None, None),
9874                };
9875                let key_ty = exp_key
9876                    .or_else(|| self.infer_homogeneous_elem_type_refs(&keys))
9877                    .unwrap_or_else(|| "interface{}".to_string());
9878                let val_ty = exp_val
9879                    .or_else(|| self.infer_homogeneous_elem_type_refs(&vals))
9880                    .unwrap_or_else(|| "interface{}".to_string());
9881                let _ = write!(self.buf, "map[{key_ty}]{val_ty}{{");
9882                for (i, entry) in entries.iter().enumerate() {
9883                    if i > 0 {
9884                        self.buf.push_str(", ");
9885                    }
9886                    self.emit_expr(&entry.key)?;
9887                    self.buf.push_str(": ");
9888                    self.emit_expr(&entry.value)?;
9889                }
9890                self.buf.push('}');
9891                Ok(())
9892            }
9893            NodeKind::SetLiteral { elems } => {
9894                // Go doesn't have sets; use map[T]struct{}. A declared `Set[T]`
9895                // binding type takes priority (empty set matches); otherwise
9896                // infer a homogeneous element type so `#{1, 2}` emits
9897                // `map[int64]struct{}{...}`.
9898                let expected = self.expected_collection_elem.take();
9899                let elem_ty = expected
9900                    .map(|(e, _)| e)
9901                    .or_else(|| self.infer_homogeneous_elem_type(elems))
9902                    .unwrap_or_else(|| "interface{}".to_string());
9903                let _ = write!(self.buf, "map[{elem_ty}]struct{{}}{{");
9904                for (i, e) in elems.iter().enumerate() {
9905                    if i > 0 {
9906                        self.buf.push_str(", ");
9907                    }
9908                    self.emit_expr(e)?;
9909                    self.buf.push_str(": {}");
9910                }
9911                self.buf.push('}');
9912                Ok(())
9913            }
9914            NodeKind::TupleLiteral { elems } => {
9915                // Go has no tuples; a `(a, b)` value is a struct with numbered
9916                // fields — the SAME representation `type_to_go` gives a tuple
9917                // *type* (`struct{ Field0 T0; Field1 T1 }`) and the match
9918                // pattern reads (`.Field0`). Emitting a `[...]interface{}` array
9919                // here (the prior lowering) produced a value whose type did not
9920                // match the `struct{…}` parameter type, so a tuple argument
9921                // failed `go build`. Build the matching struct literal instead,
9922                // inferring each field's element type (falling back to
9923                // `interface{}` when it can't be inferred).
9924                // A declared tuple return / binding type (`-> (Int, Int)` →
9925                // `struct{ Field0 int64; Field1 int64 }`) pins each field's Go
9926                // type, so a `return (count, total)` whose elements only infer to
9927                // `interface{}` (an untyped `let` binding) still emits the
9928                // concrete struct the declared return type demands. Per field:
9929                // the expected field type wins, else structural inference, else
9930                // `interface{}`.
9931                let expected_fields = self
9932                    .current_expected_type
9933                    .as_deref()
9934                    .map(Self::parse_tuple_struct_field_types)
9935                    .unwrap_or_default();
9936                let field_types: Vec<String> = elems
9937                    .iter()
9938                    .enumerate()
9939                    .map(|(i, e)| {
9940                        expected_fields
9941                            .get(i)
9942                            .cloned()
9943                            .or_else(|| self.infer_go_expr_type(e))
9944                            .unwrap_or_else(|| "interface{}".to_string())
9945                    })
9946                    .collect();
9947                let fields: Vec<String> = field_types
9948                    .iter()
9949                    .enumerate()
9950                    .map(|(i, t)| format!("Field{i} {t}"))
9951                    .collect();
9952                let _ = write!(self.buf, "struct{{ {} }}{{", fields.join("; "));
9953                for (i, e) in elems.iter().enumerate() {
9954                    if i > 0 {
9955                        self.buf.push_str(", ");
9956                    }
9957                    self.emit_expr(e)?;
9958                }
9959                self.buf.push('}');
9960                Ok(())
9961            }
9962            NodeKind::Interpolation { parts } => {
9963                self.needs_fmt_import = true;
9964                self.buf.push_str("fmt.Sprintf(\"");
9965                let mut args = Vec::new();
9966                for part in parts {
9967                    match part {
9968                        AirInterpolationPart::Literal(s) => {
9969                            // This literal lands inside a `fmt.Sprintf` FORMAT
9970                            // string, so a literal `%` must be doubled to `%%` —
9971                            // unescaped it pairs with the following bytes as a
9972                            // verb (`"${n}% pass"` → format `"%v% pass"` →
9973                            // `95%!p(MISSING)ass`), a SILENT cross-target output
9974                            // divergence: the build stays green and only Go
9975                            // corrupts (Q-go-percent-interpolation).
9976                            // `escape_go_string` never emits `%`, so escaping
9977                            // before doubling cannot double an escape.
9978                            self.buf.push_str(&escape_go_string(s).replace('%', "%%"));
9979                        }
9980                        AirInterpolationPart::Expr(expr) => {
9981                            // Q-displayable-interpolation-dispatch: render through
9982                            // `__bockStr` (a `%s` verb over a `string`) so a user
9983                            // value whose type has a `Displayable` impl (its
9984                            // `ToString` method) shows via that method, not the
9985                            // struct default (`%v` → `{3 7}`). Every other value
9986                            // falls back to `fmt.Sprintf("%v", x)` inside the
9987                            // helper, matching the prior `%v` form.
9988                            self.buf.push_str("%s");
9989                            args.push(expr.clone());
9990                        }
9991                    }
9992                }
9993                self.buf.push('"');
9994                for arg in &args {
9995                    self.buf.push_str(", __bockStr(");
9996                    self.emit_expr(arg)?;
9997                    self.buf.push(')');
9998                }
9999                self.buf.push(')');
10000                Ok(())
10001            }
10002            NodeKind::Placeholder => {
10003                self.buf.push('_');
10004                Ok(())
10005            }
10006            NodeKind::Unreachable => {
10007                self.buf.push_str("panic(\"unreachable\")");
10008                Ok(())
10009            }
10010            NodeKind::ResultConstruct { variant, value } => {
10011                // Construct the tagged Result-runtime struct (`__bockOk`/
10012                // `__bockErr`) — the same shape the surface `Ok(..)`/`Err(..)`
10013                // construction emits and the `Result` match reads on `.tag`. The
10014                // old Go-idiomatic `v, nil` / `nil, err` multi-return shape
10015                // disagreed with the tag-dispatched match, so reconcile on the
10016                // tagged struct.
10017                let ctor = match variant {
10018                    ResultVariant::Ok => "__bockOk",
10019                    ResultVariant::Err => "__bockErr",
10020                };
10021                let _ = write!(self.buf, "{ctor}(");
10022                if let Some(v) = value {
10023                    // Box a numeric-literal payload at its concrete Go type (see
10024                    // `box_payload_str`) so a later `.(int64)` / generic `.(T)`
10025                    // payload assertion does not panic on the `int` default.
10026                    if let Some(go_ty) = Self::numeric_literal_go_type(v) {
10027                        let _ = write!(self.buf, "{go_ty}(");
10028                        self.emit_expr(v)?;
10029                        self.buf.push(')');
10030                    } else {
10031                        self.emit_expr(v)?;
10032                    }
10033                } else {
10034                    self.buf.push_str("nil");
10035                }
10036                self.buf.push(')');
10037                Ok(())
10038            }
10039            NodeKind::Assign { op, target, value } => {
10040                self.emit_expr(target)?;
10041                let op_str = match op {
10042                    AssignOp::Assign => " = ",
10043                    AssignOp::AddAssign => " += ",
10044                    AssignOp::SubAssign => " -= ",
10045                    AssignOp::MulAssign => " *= ",
10046                    AssignOp::DivAssign => " /= ",
10047                    AssignOp::RemAssign => " %= ",
10048                };
10049                self.buf.push_str(op_str);
10050                self.emit_expr(value)?;
10051                Ok(())
10052            }
10053            NodeKind::If {
10054                condition,
10055                then_block,
10056                else_block,
10057                ..
10058            } => {
10059                // If in expression position: Go doesn't have ternary; emit as
10060                // IIFE. Type it with the binding's expected type when known (a
10061                // `let x: T = if …`), else the enclosing function's return type
10062                // (`func() Ordering { … }`) so a named/concrete result is
10063                // assignable; the `else` falls back to a typed zero only for the
10064                // untyped form (a concrete return type always has both branches
10065                // in a Bock `if`-expression).
10066                let iife_ty = self
10067                    .expected_iife_type()
10068                    .unwrap_or_else(|| "interface{}".to_string());
10069                // Each branch of an `if`-expression produces the SAME type as the
10070                // whole `if`, so a nested `if`/`match` in a branch tail must
10071                // inherit this IIFE's concrete type — otherwise it falls back to
10072                // the enclosing fn's return type (e.g. a nested
10073                // `func() __bockResult` inside an `else` of a `MessageType` `if`).
10074                // Propagate the concrete type into the branch bodies (a nested
10075                // `let` save/restores it, so it never wrongly overrides a binding
10076                // type); the `interface{}` fallback is left absent so the branches
10077                // keep inferring their own types.
10078                let prev_expected = self.current_expected_type.take();
10079                let branch_expected = (iife_ty != "interface{}").then(|| iife_ty.clone());
10080                // A non-`interface{}` IIFE type is the typed-zero source for a
10081                // void-call branch tail (`emit_arm_body_return`).
10082                let iife_ret = (iife_ty != "interface{}").then(|| iife_ty.clone());
10083                let _ = write!(self.buf, "func() {iife_ty} {{ if ");
10084                self.emit_expr(condition)?;
10085                self.buf.push_str(" { ");
10086                self.current_expected_type = branch_expected.clone();
10087                self.emit_arm_body_return(then_block, iife_ret.as_deref())?;
10088                self.buf.push_str(" } else { ");
10089                if let Some(eb) = else_block {
10090                    self.current_expected_type = branch_expected;
10091                    self.emit_arm_body_return(eb, iife_ret.as_deref())?;
10092                } else {
10093                    self.buf.push_str("return nil");
10094                }
10095                self.buf.push_str(" } }()");
10096                self.current_expected_type = prev_expected;
10097                Ok(())
10098            }
10099            NodeKind::Block { stmts, tail } => {
10100                if stmts.is_empty() {
10101                    if let Some(t) = tail {
10102                        return self.emit_expr(t);
10103                    }
10104                }
10105                // A block with statements in expression position (e.g. an
10106                // `if`/`match` arm body `{ let id = ...; GetUser { id: id } }`)
10107                // lowers to an IIFE that runs the statements then returns the
10108                // tail. The earlier `func() interface{} { return <tail> }` form
10109                // both DROPPED the statements (so a `let` binding used by the tail
10110                // was `undefined`) and erased the result to `interface{}` (so a
10111                // variant struct returned as the tail was not assignable to the
10112                // enclosing sealed-interface IIFE type, e.g. `Route`). Type the
10113                // IIFE with the expected type — the binding's `current_expected_type`
10114                // or the enclosing fn's return type (`expected_iife_type`) — and
10115                // emit the full block body via `emit_block_body_return`, which runs
10116                // the statements and returns the (typed) tail. A typed IIFE closes
10117                // with `panic("unreachable")` (it always returns through the tail);
10118                // the untyped form keeps `return nil`.
10119                let iife_ret = self.expected_iife_type();
10120                let iife_ty = iife_ret.as_deref().unwrap_or("interface{}");
10121                // Consume the expected type for THIS IIFE's return only; the block
10122                // body re-types its own tail (and may re-set it via a nested
10123                // `let`), so it must not leak into the statements.
10124                let prev_expected = self.current_expected_type.take();
10125                // The body's tail terminates the IIFE when it is a value
10126                // expression (emitted as `return <tail>`) or a terminating
10127                // statement (`return`/`break`/`continue`). Only when the block can
10128                // fall through (no tail, or an assignment tail) do we need the
10129                // trailing fallback — emitting it unconditionally would leave dead
10130                // code after a `return`. (Go does not reject unreachable code, but
10131                // the conditional keeps the output clean.)
10132                let body_falls_through = match tail {
10133                    Some(t) => crate::generator::node_is_statement(t) && !Self::tail_terminates(t),
10134                    None => true,
10135                };
10136                let _ = writeln!(self.buf, "func() {iife_ty} {{");
10137                self.indent += 1;
10138                self.emit_block_body_return(node)?;
10139                if body_falls_through {
10140                    if iife_ty == "interface{}" {
10141                        self.writeln("return nil");
10142                    } else {
10143                        self.writeln("panic(\"unreachable\")");
10144                    }
10145                }
10146                self.indent -= 1;
10147                self.write_indent();
10148                self.buf.push_str("}()");
10149                self.current_expected_type = prev_expected;
10150                Ok(())
10151            }
10152            NodeKind::Match { scrutinee, arms } => {
10153                // Guards, or-/tuple/list/range patterns, and nested
10154                // constructor/record patterns cannot ride the value/type
10155                // `switch` IIFE below (its `case <cond>` form has no slot for a
10156                // relational/length test or a fall-through guard — they collapse
10157                // to a broken `case interface{}:`). Route them to the shared
10158                // if/else-if chain, wrapped in a typed IIFE whose arm bodies
10159                // `return` the match's value.
10160                //
10161                // This `match_needs_ifchain` check comes BEFORE the Optional/Result
10162                // tag-switch fast-paths (mirroring the statement-position
10163                // `emit_match`, which checks it first): a *nested* Optional/Result
10164                // pattern (`Some(Ok(n))`) is structured, so the tag-switch
10165                // `emit_optional_match_expr` cannot bind its nested payload
10166                // (`undefined: n`). The if-chain's `collect_binds_go` recurses
10167                // through the nested `Some`/`Ok` tags and binds `n` off the typed
10168                // `.v` payloads. A *flat* `Some(x)`/`Ok(v)` match is not structured,
10169                // so it still takes the tag-switch fast-path below.
10170                //
10171                // Two further go.rs-local diversions (NOT in the shared
10172                // `match_needs_ifchain`, which keeps these on the switch fast-path —
10173                // correct only for *statement* position, where `emit_match` binds
10174                // `__v`): a bare-bind arm (`x => …`, `mut x => …`) has no value to
10175                // switch on, so the value-switch IIFE emits `case interface{}:` (a
10176                // *type* in expression position) and drops the name; and a *plain*
10177                // record arm (`Point { x, .. }`) is a concrete struct, not a
10178                // sealed-interface value, so `case Point:` is likewise invalid. The
10179                // if-chain IIFE tests/binds every pattern kind (a bare bind → an
10180                // unconditional `else` with `x := root`; a plain record → field
10181                // reads off `root.X`).
10182                if crate::generator::match_needs_ifchain(arms)
10183                    || Self::go_value_match_has_bind_arm(arms)
10184                    || self.go_value_match_has_plain_record_arm(arms)
10185                {
10186                    let iife_ret = self.expected_iife_type();
10187                    let iife_ty = iife_ret.as_deref().unwrap_or("interface{}");
10188                    // Each arm yields the SAME type as the whole match, so a
10189                    // nested branchy tail inherits the concrete IIFE type; a
10190                    // nested `let` save/restores it. Mirrors the switch path.
10191                    let prev_expected = self.current_expected_type.take();
10192                    self.current_expected_type =
10193                        (iife_ty != "interface{}").then(|| iife_ty.to_string());
10194                    let _ = writeln!(self.buf, "func() {iife_ty} {{");
10195                    self.indent += 1;
10196                    let res =
10197                        self.emit_match_ifchain_inner(scrutinee, arms, /*emit_return=*/ true);
10198                    self.indent -= 1;
10199                    self.current_expected_type = prev_expected;
10200                    res?;
10201                    // A Bock match is exhaustive, but Go cannot prove the if-chain
10202                    // is total, so a trailing `panic` keeps the IIFE well-typed.
10203                    self.write_indent();
10204                    self.buf.push_str("panic(\"unreachable\")\n");
10205                    self.write_indent();
10206                    self.buf.push_str("}()");
10207                    return Ok(());
10208                }
10209                // Flat `Optional` / `Result` matches dispatch on the runtime tag
10210                // (`Some(x)`/`None`, `Ok(v)`/`Err(e)`). A *nested* such match
10211                // (`Some(Ok(n))`) was diverted to the if-chain above, so only the
10212                // single-level tag-switch reaches here.
10213                if go_match_is_optional(arms) {
10214                    return self.emit_optional_match_expr(scrutinee, arms);
10215                }
10216                if go_match_is_result(arms) {
10217                    return self.emit_result_match_expr(scrutinee, arms);
10218                }
10219                // A user-enum match (including a reachable `core.compare.Ordering`
10220                // enum) dispatches on the dynamic concrete-variant *type*
10221                // (`OrderingGreater`), so the IIFE must be a *type-switch* — the
10222                // variant names are Go struct types, not values, so a value-switch
10223                // (`case OrderingGreater:`) is a compile error. (The prelude
10224                // `__bockOrdering` value-enum, used when the real enum is NOT
10225                // reachable, stays a value-switch via the path below.)
10226                let is_user_enum = self.go_match_is_user_enum(arms);
10227                // Type the IIFE so its result is assignable where a
10228                // concrete/named type is required — `interface{}` does not
10229                // satisfy a named interface like the user `Ordering`. Prefer the
10230                // binding's *expected* type (`let x: T = match …`) when known and
10231                // concrete: a value-position match binds into `T`, which need not
10232                // equal the enclosing function's return type. Otherwise fall back
10233                // to the function's return type (the return-position case:
10234                // `return match …`), preserving the working Optional/Result/enum
10235                // behavior. A typed IIFE closes with `panic("unreachable")` (a
10236                // Bock match is exhaustive) rather than `return nil`, which has no
10237                // value for a concrete return type.
10238                let iife_ret = self.expected_iife_type();
10239                let iife_ty = iife_ret.as_deref().unwrap_or("interface{}");
10240                // Consume the expected type for THIS IIFE's return only; the
10241                // scrutinee is a different (matched) type and must not inherit it.
10242                // Each arm produces the SAME type as the whole match, so a nested
10243                // `if`/`match` in an arm tail DOES inherit the concrete IIFE type
10244                // (else it falls back to the enclosing fn's return type). A nested
10245                // `let` save/restores it, so it never wrongly overrides a binding.
10246                let prev_expected = self.current_expected_type.take();
10247                let arm_expected = (iife_ty != "interface{}").then(|| iife_ty.to_string());
10248                // A user-enum match whose arms extract payload fields
10249                // (`Heading { level, text } => …`) must narrow on `__v` so each
10250                // case can read `__v.Level` / `__v.Text`. A payload-less enum
10251                // match (the `core.compare.Ordering` unit variants) keeps the
10252                // non-binding `switch x.(type)` (binding `__v` would be Go's
10253                // "declared and not used").
10254                let user_enum_binds = is_user_enum && Self::go_user_enum_match_binds_payload(arms);
10255                let _ = write!(self.buf, "func() {iife_ty} {{ switch ");
10256                if user_enum_binds {
10257                    self.buf.push_str("__v := ");
10258                    self.emit_expr(scrutinee)?;
10259                    self.buf.push_str(".(type) { ");
10260                } else if is_user_enum {
10261                    // Non-binding type-switch (`switch x.(type)`): the
10262                    // `core.compare.Ordering` variants are unit (no payload), so
10263                    // no `__v` binding is needed, which also avoids Go's
10264                    // "declared and not used" on a payload-less match.
10265                    self.emit_expr(scrutinee)?;
10266                    self.buf.push_str(".(type) { ");
10267                } else {
10268                    self.emit_expr(scrutinee)?;
10269                    self.buf.push_str(" { ");
10270                }
10271                // Match in expression position: emit as IIFE with switch. Each
10272                // arm body is terminated with `;` so consecutive single-line
10273                // `case`/`default` clauses are separated — Go requires a
10274                // statement terminator between a `case` body's trailing `return`
10275                // and the next `case`/`default` keyword (a bare space is a
10276                // "unexpected keyword case" syntax error).
10277                for arm in arms {
10278                    if let NodeKind::MatchArm { pattern, body, .. } = &arm.kind {
10279                        if matches!(pattern.kind, NodeKind::WildcardPat) {
10280                            self.buf.push_str("default: ");
10281                        } else {
10282                            self.buf.push_str("case ");
10283                            self.emit_match_case_condition(pattern)?;
10284                            self.buf.push_str(": ");
10285                        }
10286                        // Bind the narrowed `__v`'s payload fields for this arm
10287                        // (`level := __v.Level; _ = level`) before the body.
10288                        if user_enum_binds {
10289                            self.emit_user_enum_arm_bindings(pattern)?;
10290                        }
10291                        self.current_expected_type = arm_expected.clone();
10292                        self.emit_arm_body_return(body, iife_ret.as_deref())?;
10293                        self.buf.push_str("; ");
10294                    }
10295                }
10296                // `}; <fallthrough>` — the switch's closing brace and the IIFE's
10297                // fallthrough are two statements on one line, so they need an
10298                // explicit separator (a bare `} return` is a Go syntax error:
10299                // "unexpected keyword return at end of statement"). A typed IIFE
10300                // uses `panic` (no `nil` for a concrete type); the untyped form
10301                // keeps `return nil`.
10302                if iife_ret.is_some() {
10303                    self.buf.push_str("}; panic(\"unreachable\") }()");
10304                } else {
10305                    self.buf.push_str("}; return nil }()");
10306                }
10307                self.current_expected_type = prev_expected;
10308                Ok(())
10309            }
10310            // Ownership nodes: erase in Go.
10311            NodeKind::Move { expr }
10312            | NodeKind::Borrow { expr }
10313            | NodeKind::MutableBorrow { expr } => self.emit_expr(expr),
10314            // Effect operation invocation.
10315            NodeKind::EffectOp {
10316                effect,
10317                operation,
10318                args,
10319            } => {
10320                let effect_name = effect.segments.last().map_or("effect", |s| s.name.as_str());
10321                let _ = write!(
10322                    self.buf,
10323                    "{}.{}",
10324                    to_camel_case(effect_name),
10325                    to_pascal_case(&operation.name)
10326                );
10327                self.buf.push('(');
10328                for (i, arg) in args.iter().enumerate() {
10329                    if i > 0 {
10330                        self.buf.push_str(", ");
10331                    }
10332                    self.emit_expr(&arg.value)?;
10333                }
10334                self.buf.push(')');
10335                Ok(())
10336            }
10337            // Type expressions: erased in Go expression context.
10338            NodeKind::TypeNamed { .. }
10339            | NodeKind::TypeTuple { .. }
10340            | NodeKind::TypeFunction { .. }
10341            | NodeKind::TypeOptional { .. }
10342            | NodeKind::TypeSelf => {
10343                self.buf.push_str("/* type */");
10344                Ok(())
10345            }
10346            NodeKind::EffectRef { path } => {
10347                let name = path
10348                    .segments
10349                    .iter()
10350                    .map(|s| s.name.as_str())
10351                    .collect::<Vec<_>>()
10352                    .join(".");
10353                self.buf.push_str(&name);
10354                Ok(())
10355            }
10356            NodeKind::Error => {
10357                self.buf.push_str("/* error */");
10358                Ok(())
10359            }
10360            // An expression-position `loop` (`let r = loop { … break <v> }`):
10361            // Bock's `loop` is a value-producing expression terminated by a
10362            // `break <v>`. Go's `for` carries no value, so lower it to an IIFE
10363            // `func() T { for { … } }()` whose body's `break <v>` becomes
10364            // `return <v>` (handled via `loop_expr_depth` in the `Break` arm). The
10365            // loop is infinite — it only leaves through a `break <v>` — so the
10366            // trailing `panic("unreachable")` after the `for` is never reached but
10367            // satisfies Go's "missing return" check.
10368            NodeKind::Loop { body } => {
10369                let iife_ret = self.expected_iife_type();
10370                let iife_ty = iife_ret.as_deref().unwrap_or("interface{}");
10371                // The IIFE consumes the expected type for its own return; the loop
10372                // body re-types its own `break` values, so it must not leak into
10373                // the statements within.
10374                let prev_expected = self.current_expected_type.take();
10375                let _ = writeln!(self.buf, "func() {iife_ty} {{");
10376                self.indent += 1;
10377                self.loop_expr_depth += 1;
10378                self.emit_loop_label_prefix(body);
10379                self.writeln("for {");
10380                self.indent += 1;
10381                self.emit_block_body(body)?;
10382                self.indent -= 1;
10383                self.writeln("}");
10384                self.loop_labels.pop();
10385                self.loop_expr_depth -= 1;
10386                if iife_ty == "interface{}" {
10387                    self.writeln("return nil");
10388                } else {
10389                    self.writeln("panic(\"unreachable\")");
10390                }
10391                self.indent -= 1;
10392                self.write_indent();
10393                self.buf.push_str("}()");
10394                self.current_expected_type = prev_expected;
10395                Ok(())
10396            }
10397            _ => {
10398                self.buf.push_str("/* unsupported */");
10399                Ok(())
10400            }
10401        }
10402    }
10403
10404    // ── Match → switch/if-else ──────────────────────────────────────────────
10405
10406    /// Push a loop scope, emitting a Go label before the loop iff a contained
10407    /// statement-arm `match` needs to `break`/`continue` the loop (Go's `break`
10408    /// otherwise exits the inner `switch`). Must be paired with a
10409    /// `self.loop_labels.pop()` after the loop body is emitted.
10410    fn emit_loop_label_prefix(&mut self, body: &AIRNode) {
10411        if go_loop_needs_label(body) {
10412            self.loop_label_counter += 1;
10413            let label = format!("__bockLoop{}", self.loop_label_counter);
10414            let ind = self.indent_str();
10415            // A Go label sits in column-0-ish; we keep current indent for
10416            // readability — gofmt would re-align but the program is valid.
10417            let _ = writeln!(self.buf, "{ind}{label}:");
10418            self.loop_labels.push(Some(label));
10419        } else {
10420            self.loop_labels.push(None);
10421        }
10422    }
10423
10424    /// The label of the innermost loop, if one was allocated. Used by
10425    /// `break`/`continue` emitted inside a statement-arm `switch`.
10426    fn innermost_loop_label(&self) -> Option<&str> {
10427        self.loop_labels.last().and_then(|l| l.as_deref())
10428    }
10429
10430    /// Emit an `Optional` `match` in expression position as an IIFE that
10431    /// dispatches on the runtime tag (`__bockOption.tag`). `Some(v)` binds
10432    /// `v` from `.v` (as `interface{}`); `None`/`_` is the fallthrough.
10433    fn emit_optional_match_expr(
10434        &mut self,
10435        scrutinee: &AIRNode,
10436        arms: &[AIRNode],
10437    ) -> Result<(), CodegenError> {
10438        let elem = self.scrutinee_optional_elem(scrutinee);
10439        // Type the IIFE with the binding's *expected* type (a typed `let x: T =
10440        // match …` or a function-return `return match …`, both surfaced through
10441        // `current_expected_type`) when concrete, so the IIFE result is
10442        // assignable to the destination — a bare `interface{}` IIFE is not
10443        // assignable to a named `__bockOption` / `[]T` / `int64` / `T` return.
10444        // When no concrete expected type is in scope (e.g. the match is nested
10445        // in a string interpolation, whose `%v` consumes `interface{}`), fall
10446        // back to the untyped IIFE, preserving the existing behavior.
10447        let iife_ret = self.typed_match_iife_type();
10448        let iife_ty = iife_ret.as_deref().unwrap_or("interface{}");
10449        let prev_expected = self.current_expected_type.take();
10450        // A collection-typed IIFE (`func() []T { … }`) propagates its element
10451        // type to the arm bodies' collection literals, so `to_list`'s `[x]`/`[]`
10452        // arms emit `[]T{x}` / `[]T{}` rather than the `[]interface{}` default
10453        // (which is not assignable to the `[]T` return). Re-applied per arm
10454        // inside the arms emitter (a literal `take()`s the hint, so a single
10455        // top-level set would only reach the first arm).
10456        let iife_coll = iife_ret.as_deref().and_then(Self::rendered_collection_elem);
10457        let _ = write!(self.buf, "func() {iife_ty} {{ __opt := ");
10458        self.emit_expr(scrutinee)?;
10459        self.buf.push_str("; ");
10460        self.emit_optional_match_arms(
10461            arms,
10462            /*as_expr=*/ true,
10463            elem.as_deref(),
10464            iife_ret.is_some(),
10465            iife_coll.as_ref(),
10466            iife_ret.as_deref(),
10467        )?;
10468        self.buf.push_str(" }()");
10469        self.current_expected_type = prev_expected;
10470        Ok(())
10471    }
10472
10473    /// Parse a rendered Go collection type string into its element Go type(s) for
10474    /// [`Self::expected_collection_elem`]: `[]int64` → `("int64", None)`,
10475    /// `map[string]int64` → `("string", Some("int64"))`. `None` for a
10476    /// non-collection rendering. Used to propagate a collection-typed IIFE
10477    /// return into the arm bodies' literals.
10478    fn rendered_collection_elem(ty: &str) -> Option<(String, Option<String>)> {
10479        let ty = ty.trim();
10480        if let Some(elem) = ty.strip_prefix("[]") {
10481            return Some((elem.to_string(), None));
10482        }
10483        if let Some(rest) = ty.strip_prefix("map[") {
10484            // Split on the matching close bracket of the key type (top-level).
10485            let mut depth = 0i32;
10486            for (i, ch) in rest.char_indices() {
10487                match ch {
10488                    '[' => depth += 1,
10489                    ']' if depth == 0 => {
10490                        let key = rest[..i].to_string();
10491                        let val = rest[i + 1..].to_string();
10492                        return Some((key, Some(val)));
10493                    }
10494                    ']' => depth -= 1,
10495                    _ => {}
10496                }
10497            }
10498        }
10499        None
10500    }
10501
10502    /// The Go type a typed expression-position `Optional`/`Result` match IIFE
10503    /// should return: the binding's *expected* type ([`Self::current_expected_type`],
10504    /// set around a typed `let x: T = …` value emit or a function-return tail)
10505    /// when known and concrete. Unlike [`Self::expected_iife_type`] this does
10506    /// **not** fall back to the enclosing function's return type: an
10507    /// `Optional`/`Result` match nested in a non-return position (a string
10508    /// interpolation argument, say) must stay the untyped `interface{}` IIFE its
10509    /// `%v` consumer expects, never adopt the function's unrelated return type.
10510    /// `None` ⇒ the caller emits the `interface{}` fallback.
10511    fn typed_match_iife_type(&self) -> Option<String> {
10512        match self.current_expected_type.as_deref() {
10513            Some(t) if t != "interface{}" => Some(t.to_string()),
10514            _ => None,
10515        }
10516    }
10517
10518    /// True when `node` is an *expression-position* `Optional`/`Result` match —
10519    /// a `match` over `Some`/`None` or `Ok`/`Err` whose arms are all
10520    /// value-producing (no statement arm). Such a match lowers to a typed IIFE
10521    /// (`func() __bockOption { … }()`) whose return must be assignable to the
10522    /// enclosing return/binding type; the callers use this to propagate the
10523    /// expected type into [`Self::current_expected_type`], mirroring the
10524    /// generic-record-construction case.
10525    fn is_expr_optional_or_result_match(node: &AIRNode) -> bool {
10526        if let NodeKind::Match { arms, .. } = &node.kind {
10527            !crate::generator::match_has_statement_arm(arms)
10528                && (go_match_is_optional(arms) || go_match_is_result(arms))
10529        } else {
10530            false
10531        }
10532    }
10533
10534    /// Emit an `Optional` `match` in statement position as an if/else chain on
10535    /// the runtime tag.
10536    fn emit_optional_match_stmt(
10537        &mut self,
10538        scrutinee: &AIRNode,
10539        arms: &[AIRNode],
10540    ) -> Result<(), CodegenError> {
10541        let elem = self.scrutinee_optional_elem(scrutinee);
10542        // Wrap the `__opt := …` binding + tag chain in a Go block so the scrutinee
10543        // temp is scoped to this match. Two sequential statement matches in one
10544        // function (`match a {…}` then `match b {…}`) otherwise both emit
10545        // `__opt := …` in the same scope → `no new variables on left side of :=`.
10546        let ind = self.indent_str();
10547        let _ = writeln!(self.buf, "{ind}{{");
10548        self.indent += 1;
10549        let ind2 = self.indent_str();
10550        let _ = write!(self.buf, "{ind2}__opt := ");
10551        self.emit_expr(scrutinee)?;
10552        self.buf.push('\n');
10553        self.write_indent();
10554        self.emit_optional_match_arms(
10555            arms,
10556            /*as_expr=*/ false,
10557            elem.as_deref(),
10558            false,
10559            None,
10560            None,
10561        )?;
10562        self.buf.push('\n');
10563        self.indent -= 1;
10564        self.writeln("}");
10565        Ok(())
10566    }
10567
10568    /// Shared body for [`emit_optional_match_expr`] /
10569    /// [`emit_optional_match_stmt`]: an if/else chain on the option tag. In
10570    /// expression mode each arm body is `return`ed; in statement mode the arm
10571    /// body is emitted as a block.
10572    #[allow(clippy::too_many_arguments)]
10573    fn emit_optional_match_arms(
10574        &mut self,
10575        arms: &[AIRNode],
10576        as_expr: bool,
10577        some_elem_ty: Option<&str>,
10578        typed_iife: bool,
10579        iife_coll: Option<&(String, Option<String>)>,
10580        iife_ty: Option<&str>,
10581    ) -> Result<(), CodegenError> {
10582        // Save the caller's pending collection-element hint: the arm bodies
10583        // re-set it per-arm (`iife_coll`), so it must be restored on exit rather
10584        // than clobbered (a match-expr may itself be a collection-literal element).
10585        let saved_coll = self.expected_collection_elem.take();
10586        let mut first = true;
10587        let arm_count = arms.len();
10588        for (idx, arm) in arms.iter().enumerate() {
10589            let NodeKind::MatchArm { pattern, body, .. } = &arm.kind else {
10590                continue;
10591            };
10592            let is_last = idx + 1 == arm_count;
10593            // Determine the tag test and any bound name. The final arm is
10594            // rendered as a plain `else` so the if-chain is exhaustive from
10595            // Go\'s control-flow view (Bock matches are exhaustive). Its bound
10596            // name (e.g. the `Some(v)` value) is still extracted.
10597            // `bind` is the payload name (the `v` in `Some(v)`); `bind_is_payload`
10598            // is true only when it binds the `Some` payload (not a catch-all
10599            // binding of the whole option), so the `interface{}` payload type
10600            // assertion applies to exactly that case.
10601            let (cond, bind, bind_is_payload): (String, Option<String>, bool) = match &pattern.kind
10602            {
10603                NodeKind::ConstructorPat { path, fields } => {
10604                    let variant = path.segments.last().map_or("", |s| s.name.as_str());
10605                    let bind = fields.first().map(|f| self.pattern_to_binding_name(f));
10606                    let is_payload = bind.is_some() && variant == "Some";
10607                    if is_last {
10608                        (String::new(), bind, is_payload)
10609                    } else {
10610                        (format!("__opt.tag == \"{variant}\""), bind, is_payload)
10611                    }
10612                }
10613                // Wildcard / bind pattern → catch-all.
10614                _ => (String::new(), None, false),
10615            };
10616            if first {
10617                first = false;
10618                if cond.is_empty() {
10619                    self.buf.push('{');
10620                } else {
10621                    let _ = write!(self.buf, "if {cond} {{");
10622                }
10623            } else if cond.is_empty() {
10624                self.buf.push_str(" else {");
10625            } else {
10626                let _ = write!(self.buf, " else if {cond} {{");
10627            }
10628            self.buf.push(' ');
10629            if let Some(name) = &bind {
10630                if name != "_" {
10631                    // The runtime stores the payload as `interface{}`. Assert it
10632                    // back to the concrete element type so typed use of the bound
10633                    // value (`x + 10`, a typed call) compiles. The element type
10634                    // comes from the scrutinee's `Optional[T]` (resolved
10635                    // structurally by the caller); when unknown, fall back to the
10636                    // bare `interface{}` payload — no regression, but typed use
10637                    // would not compile, which only happens if the scrutinee's
10638                    // element type is not structurally determinable.
10639                    match (bind_is_payload, some_elem_ty) {
10640                        // Numeric element types are recovered through the
10641                        // widening helpers rather than a hard `.(int64)` /
10642                        // `.(float64)` assertion: a payload constructed from an
10643                        // untyped Go constant (`Some(10)` → `__bockSome(10)`)
10644                        // boxes a Go `int`/`float64`, on which `.(int64)` panics.
10645                        (true, Some("int64")) => {
10646                            let _ =
10647                                write!(self.buf, "{name} := __bockAsInt64(__opt.v); _ = {name}; ");
10648                        }
10649                        (true, Some("float64")) => {
10650                            let _ = write!(
10651                                self.buf,
10652                                "{name} := __bockAsFloat64(__opt.v); _ = {name}; "
10653                            );
10654                        }
10655                        (true, Some(ty)) => {
10656                            let _ = write!(self.buf, "{name} := __opt.v.({ty}); _ = {name}; ");
10657                        }
10658                        _ => {
10659                            let _ = write!(self.buf, "{name} := __opt.v; _ = {name}; ");
10660                        }
10661                    }
10662                }
10663            }
10664            if as_expr {
10665                // Re-apply the IIFE's collection element per arm: a list/map/set
10666                // literal `take()`s the hint, so without re-setting it only the
10667                // first arm's literal would adopt the `[]T` element (`to_list`'s
10668                // `Some` arm), leaving the `None` arm's `[]` as `[]interface{}`.
10669                self.expected_collection_elem = iife_coll.cloned();
10670                // A void-call arm tail emits as a statement + discarded zero
10671                // (`return println(..)` is a Go arity error).
10672                self.emit_arm_body_return(body, iife_ty)?;
10673                self.buf.push(' ');
10674            } else {
10675                self.buf.push('\n');
10676                self.indent += 1;
10677                self.emit_block_body(body)?;
10678                self.indent -= 1;
10679                self.write_indent();
10680            }
10681            self.buf.push('}');
10682        }
10683        self.expected_collection_elem = saved_coll;
10684        // Expression mode needs a trailing value if no arm matched. A `;`
10685        // separates it from the preceding `}` (Go requires a terminator). A
10686        // *typed* IIFE has no `nil` of its concrete return type, so it closes
10687        // with `panic` (a Bock match is exhaustive, so this is unreachable);
10688        // the untyped form keeps `return nil`.
10689        if as_expr {
10690            if typed_iife {
10691                self.buf.push_str("; panic(\"unreachable\")");
10692            } else {
10693                self.buf.push_str("; return nil");
10694            }
10695        }
10696        Ok(())
10697    }
10698
10699    /// Emit a `Result` `match` in expression position as an IIFE that dispatches
10700    /// on the runtime tag (`__bockResult.tag`). Mirrors
10701    /// [`Self::emit_optional_match_expr`].
10702    fn emit_result_match_expr(
10703        &mut self,
10704        scrutinee: &AIRNode,
10705        arms: &[AIRNode],
10706    ) -> Result<(), CodegenError> {
10707        let elems = self.scrutinee_result_elems(scrutinee);
10708        // Type the IIFE with the expected destination type when concrete (see
10709        // [`Self::emit_optional_match_expr`]); else the untyped fallback.
10710        let iife_ret = self.typed_match_iife_type();
10711        let iife_ty = iife_ret.as_deref().unwrap_or("interface{}");
10712        let prev_expected = self.current_expected_type.take();
10713        let iife_coll = iife_ret.as_deref().and_then(Self::rendered_collection_elem);
10714        let _ = write!(self.buf, "func() {iife_ty} {{ __res := ");
10715        self.emit_expr(scrutinee)?;
10716        self.buf.push_str("; ");
10717        self.emit_result_match_arms(
10718            arms,
10719            /*as_expr=*/ true,
10720            elems.as_ref(),
10721            iife_ret.is_some(),
10722            iife_coll.as_ref(),
10723            iife_ret.as_deref(),
10724        )?;
10725        self.buf.push_str(" }()");
10726        self.current_expected_type = prev_expected;
10727        Ok(())
10728    }
10729
10730    /// Emit a `Result` `match` in statement position as an if/else chain on the
10731    /// runtime tag. Mirrors [`Self::emit_optional_match_stmt`].
10732    fn emit_result_match_stmt(
10733        &mut self,
10734        scrutinee: &AIRNode,
10735        arms: &[AIRNode],
10736    ) -> Result<(), CodegenError> {
10737        let elems = self.scrutinee_result_elems(scrutinee);
10738        // Scope the `__res := …` temp to a Go block so sequential statement
10739        // matches in one function don't collide on `__res` (`no new variables on
10740        // left side of :=`). See `emit_optional_match_stmt`.
10741        let ind = self.indent_str();
10742        let _ = writeln!(self.buf, "{ind}{{");
10743        self.indent += 1;
10744        let ind2 = self.indent_str();
10745        let _ = write!(self.buf, "{ind2}__res := ");
10746        self.emit_expr(scrutinee)?;
10747        self.buf.push('\n');
10748        self.write_indent();
10749        self.emit_result_match_arms(
10750            arms,
10751            /*as_expr=*/ false,
10752            elems.as_ref(),
10753            false,
10754            None,
10755            None,
10756        )?;
10757        self.buf.push('\n');
10758        self.indent -= 1;
10759        self.writeln("}");
10760        Ok(())
10761    }
10762
10763    /// Shared body for the `Result` match emitters: an if/else chain on the
10764    /// result tag. `Ok(v)` binds `v` from `__res.v` asserted to the Ok type;
10765    /// `Err(e)` binds `e` asserted to the Err type. Mirrors
10766    /// [`Self::emit_optional_match_arms`].
10767    #[allow(clippy::too_many_arguments)]
10768    fn emit_result_match_arms(
10769        &mut self,
10770        arms: &[AIRNode],
10771        as_expr: bool,
10772        elems: Option<&(String, String)>,
10773        typed_iife: bool,
10774        iife_coll: Option<&(String, Option<String>)>,
10775        iife_ty: Option<&str>,
10776    ) -> Result<(), CodegenError> {
10777        // See `emit_optional_match_arms`: preserve the caller's pending
10778        // collection-element hint across the per-arm re-application.
10779        let saved_coll = self.expected_collection_elem.take();
10780        let mut first = true;
10781        let arm_count = arms.len();
10782        for (idx, arm) in arms.iter().enumerate() {
10783            let NodeKind::MatchArm { pattern, body, .. } = &arm.kind else {
10784                continue;
10785            };
10786            let is_last = idx + 1 == arm_count;
10787            // `(cond, bind, variant)`: the final arm is a plain `else` (Bock
10788            // matches are exhaustive), but its payload bind is still extracted.
10789            let (cond, bind, variant): (String, Option<String>, Option<&str>) = match &pattern.kind
10790            {
10791                NodeKind::ConstructorPat { path, fields } => {
10792                    let variant = path.segments.last().map_or("", |s| s.name.as_str());
10793                    let bind = fields.first().map(|f| self.pattern_to_binding_name(f));
10794                    if is_last {
10795                        (String::new(), bind, Some(variant))
10796                    } else {
10797                        (format!("__res.tag == \"{variant}\""), bind, Some(variant))
10798                    }
10799                }
10800                _ => (String::new(), None, None),
10801            };
10802            if first {
10803                first = false;
10804                if cond.is_empty() {
10805                    self.buf.push('{');
10806                } else {
10807                    let _ = write!(self.buf, "if {cond} {{");
10808                }
10809            } else if cond.is_empty() {
10810                self.buf.push_str(" else {");
10811            } else {
10812                let _ = write!(self.buf, " else if {cond} {{");
10813            }
10814            self.buf.push(' ');
10815            if let Some(name) = &bind {
10816                if name != "_" {
10817                    // Assert the `interface{}` payload to the concrete Ok/Err
10818                    // type. Numeric payloads from untyped Go constants are widened
10819                    // via the shared helpers (a hard `.(int64)` would panic, see
10820                    // `NUMERIC_RUNTIME_GO`). When the type is unknown, bind the
10821                    // bare `interface{}` payload (never wrong, only un-asserted).
10822                    let payload_ty = match (variant, elems) {
10823                        (Some("Ok"), Some((ok, _))) => Some(ok.as_str()),
10824                        (Some("Err"), Some((_, err))) => Some(err.as_str()),
10825                        _ => None,
10826                    };
10827                    match payload_ty {
10828                        Some("int64") => {
10829                            let _ =
10830                                write!(self.buf, "{name} := __bockAsInt64(__res.v); _ = {name}; ");
10831                        }
10832                        Some("float64") => {
10833                            let _ = write!(
10834                                self.buf,
10835                                "{name} := __bockAsFloat64(__res.v); _ = {name}; "
10836                            );
10837                        }
10838                        Some(ty) => {
10839                            let _ = write!(self.buf, "{name} := __res.v.({ty}); _ = {name}; ");
10840                        }
10841                        None => {
10842                            let _ = write!(self.buf, "{name} := __res.v; _ = {name}; ");
10843                        }
10844                    }
10845                }
10846            }
10847            if as_expr {
10848                // See `emit_optional_match_arms`: re-apply the IIFE collection
10849                // element per arm so every arm's literal adopts it, not just the
10850                // first.
10851                self.expected_collection_elem = iife_coll.cloned();
10852                // A void-call arm tail (`Err(e) => println(..)`) must emit the
10853                // call as a statement + discarded zero, not `return println(..)`
10854                // (a Go arity error: `fmt.Println` returns `(int, error)`).
10855                self.emit_arm_body_return(body, iife_ty)?;
10856                self.buf.push(' ');
10857            } else {
10858                self.buf.push('\n');
10859                self.indent += 1;
10860                self.emit_block_body(body)?;
10861                self.indent -= 1;
10862                self.write_indent();
10863            }
10864            self.buf.push('}');
10865        }
10866        self.expected_collection_elem = saved_coll;
10867        if as_expr {
10868            if typed_iife {
10869                self.buf.push_str("; panic(\"unreachable\")");
10870            } else {
10871                self.buf.push_str("; return nil");
10872            }
10873        }
10874        Ok(())
10875    }
10876
10877    fn emit_match(&mut self, scrutinee: &AIRNode, arms: &[AIRNode]) -> Result<(), CodegenError> {
10878        // Guards, or-patterns, tuple patterns, and nested constructor/record
10879        // patterns cannot be expressed by the value/type `switch` below (a
10880        // failed guard's `break` exits the switch; an or-pattern has no single
10881        // discriminant; a nested sub-pattern's bindings are lost). Lower those
10882        // to an if/else-if chain. This takes priority over the Optional/Result
10883        // fast-paths so e.g. `Some(Ok(v))` (an Optional-leaf match that is still
10884        // nested) routes here. Additive: everything else keeps its existing
10885        // switch / tag-chain lowering (see `match_needs_ifchain`).
10886        if crate::generator::match_needs_ifchain(arms) {
10887            return self.emit_match_ifchain(scrutinee, arms);
10888        }
10889        // A plain (non-enum-variant) record pattern with only bind/wildcard
10890        // fields (`Point { x, .. }`) is a concrete Go struct, not a
10891        // sealed-interface value: it has no type/value to `switch` on
10892        // (`switch __v.(type) { case Point: }` is invalid — `__v` is not an
10893        // interface). Route it to the if-chain, which reads each field directly
10894        // off `access.<Field>`. Mirrors the expression-position diversion. Does
10895        // not touch the shared `match_needs_ifchain`.
10896        if self.go_value_match_has_plain_record_arm(arms) {
10897            return self.emit_match_ifchain(scrutinee, arms);
10898        }
10899        // `Optional` / `Result` matches dispatch on the runtime tag, not a Go
10900        // type/value switch.
10901        if go_match_is_optional(arms) {
10902            return self.emit_optional_match_stmt(scrutinee, arms);
10903        }
10904        if go_match_is_result(arms) {
10905            return self.emit_result_match_stmt(scrutinee, arms);
10906        }
10907        // A user enum lowers to a type-switch over the sealed-interface concrete
10908        // variant structs, binding each arm's payload fields from `__v`.
10909        let user_enum = self.go_match_is_user_enum(arms);
10910        // The prelude `Ordering` is a `__bockOrdering` *value* enum (constants),
10911        // so its match is a value-switch (`switch o { case Less: }`), never the
10912        // type-switch user enums use — `Less` is a constant, not a Go type.
10913        let ordering = !user_enum && go_match_is_ordering(arms);
10914        // Choose value-switch (`switch v { case 5: }`) vs type-switch
10915        // (`switch v := s.(type) { case T: }`) by pattern kind: constructor /
10916        // record patterns dispatch on dynamic type; literal / bind patterns
10917        // dispatch on value. `Ordering` is forced to a value-switch.
10918        let type_switch = !ordering && (user_enum || go_match_is_type_switch(arms));
10919        // A value-switch arm may bind the whole scrutinee (`x => …`). The
10920        // scrutinee is bound into `__v` via the switch's init clause so the arm
10921        // can emit `x := __v` — without this the `default:` discarded the name
10922        // and the body referenced an undefined variable (the Go binding-drop
10923        // defect). Only needed for the value-switch path; the type-switches
10924        // already bind `__v`.
10925        let value_switch_binds = !user_enum
10926            && !type_switch
10927            && arms.iter().any(|arm| {
10928                matches!(
10929                    &arm.kind,
10930                    NodeKind::MatchArm { pattern, .. } if matches!(pattern.kind, NodeKind::BindPat { .. })
10931                )
10932            });
10933        // The user-enum type-switch binds `__v` only when something reads it:
10934        // an arm that extracts a payload field (`__v.Radius`) or the trailing
10935        // `default: panic(... __v)` added for a non-exhaustive (catch-all-free)
10936        // match. A payload-less, catch-all-bearing match (e.g. `match ord {
10937        // Greater => …  _ => {} }`) binds nothing — emit a non-binding
10938        // `switch s.(type)` so Go does not reject an unused `__v`. Mirrors the
10939        // expression-position IIFE path (see `emit_match` expr lowering).
10940        let user_enum_default_panic = user_enum && !go_match_has_default_arm(arms);
10941        let user_enum_binds_v =
10942            user_enum && (Self::go_user_enum_match_binds_payload(arms) || user_enum_default_panic);
10943        let ind = self.indent_str();
10944        if user_enum && !user_enum_binds_v {
10945            // Non-binding type-switch: no arm reads `__v` and no `__v`-consuming
10946            // default panic follows, so binding it would be "declared and not
10947            // used".
10948            let _ = write!(self.buf, "{ind}switch ");
10949            self.emit_expr(scrutinee)?;
10950            self.buf.push_str(".(type) {\n");
10951        } else if user_enum {
10952            // A *narrowing* type-switch: `switch __v := s.(type)` rebinds `__v`
10953            // to the concrete variant struct in each case, so the arm can read
10954            // its payload fields (`__v.Radius`). (The non-narrowing
10955            // `switch __v := s; __v.(type)` form does not give `__v` the
10956            // concrete type in the cases.)
10957            let _ = write!(self.buf, "{ind}switch __v := ");
10958            self.emit_expr(scrutinee)?;
10959            self.buf.push_str(".(type) {\n");
10960        } else if type_switch {
10961            let _ = write!(self.buf, "{ind}switch __v := ");
10962            self.emit_expr(scrutinee)?;
10963            self.buf.push_str("; __v.(type) {\n");
10964        } else if value_switch_binds {
10965            // `switch __v := <scrutinee>; __v { … }` — evaluate once, give the
10966            // value a name so a bind arm can alias it.
10967            let _ = write!(self.buf, "{ind}switch __v := ");
10968            self.emit_expr(scrutinee)?;
10969            self.buf.push_str("; __v {\n");
10970        } else {
10971            let _ = write!(self.buf, "{ind}switch ");
10972            self.emit_expr(scrutinee)?;
10973            self.buf.push_str(" {\n");
10974        }
10975        self.indent += 1;
10976        self.switch_label_depth += 1;
10977        // A generic user enum's variant structs carry the enum's type params, so
10978        // each `case BoxFull[int64]:` must spell the concrete instantiation
10979        // recovered from the scrutinee's type. Record it for the case emitter and
10980        // restore afterwards (matches nest). Empty for non-generic / non-user-enum
10981        // matches, leaving the bare `case ShapeCircle:` form unchanged.
10982        let new_match_args = if user_enum {
10983            self.match_enum_type_arg_suffix(scrutinee, arms)
10984        } else {
10985            String::new()
10986        };
10987        let prev_match_args =
10988            std::mem::replace(&mut self.current_match_enum_type_args, new_match_args);
10989        for arm in arms {
10990            self.emit_match_arm(arm, user_enum, value_switch_binds)?;
10991        }
10992        self.current_match_enum_type_args = prev_match_args;
10993        // Bock matches are exhaustive, but Go can't prove a type-switch covers
10994        // every implementor of a sealed interface (nor a value-switch every
10995        // `__bockOrdering` constant), so a function that returns a value after
10996        // the switch would fail to compile ("missing return"). When no arm is a
10997        // catch-all (`_` / bind), add a `default: panic(...)` so the switch is
10998        // total from Go's control-flow view.
10999        if (user_enum || ordering) && !go_match_has_default_arm(arms) {
11000            let di = self.indent_str();
11001            if user_enum {
11002                self.needs_fmt_import = true;
11003                let _ = write!(
11004                    self.buf,
11005                    "{di}default:\n{di}\tpanic(fmt.Sprintf(\"unreachable match arm: %v\", __v))\n"
11006                );
11007            } else {
11008                // Value-switch (`Ordering`): the scrutinee is not bound to a
11009                // local, so panic with a static message.
11010                let _ = write!(
11011                    self.buf,
11012                    "{di}default:\n{di}\tpanic(\"unreachable match arm\")\n"
11013                );
11014            }
11015        }
11016        self.switch_label_depth -= 1;
11017        self.indent -= 1;
11018        self.writeln("}");
11019        Ok(())
11020    }
11021
11022    // ── Match → if/else-if chain (guards, or-/tuple/nested patterns) ──────────
11023
11024    /// Lower a `match` whose arms cannot be expressed by a value/type `switch`
11025    /// (see [`crate::generator::match_needs_ifchain`]) to an `if <test> {
11026    /// <binds>; <body> } else if …` chain.
11027    ///
11028    /// The scrutinee is evaluated once into `__match` (a typed local), so nested
11029    /// tests/binds read off a single stable, typed value. Each arm contributes
11030    /// one `if`/`else if`; an unguarded catch-all (or the final unguarded arm,
11031    /// since Bock matches are exhaustive) becomes the unconditional `else`. A
11032    /// chain not closed by an `else` gets a trailing `else { panic(...) }` so a
11033    /// value-returning function still compiles (Go cannot prove exhaustiveness).
11034    ///
11035    /// Unlike the `switch` lowering, a bare `break`/`continue` in an arm body
11036    /// targets the enclosing `for` directly (there is no switch to escape), so
11037    /// `switch_label_depth` is deliberately left untouched.
11038    ///
11039    /// Statement position: arm bodies run as statements (`emit_return = false`).
11040    /// The expression-position caller (`func() T { … }()`) passes
11041    /// `emit_return = true` so each arm body's tail becomes a `return`, yielding
11042    /// the match's value.
11043    fn emit_match_ifchain(
11044        &mut self,
11045        scrutinee: &AIRNode,
11046        arms: &[AIRNode],
11047    ) -> Result<(), CodegenError> {
11048        self.emit_match_ifchain_inner(scrutinee, arms, /*emit_return=*/ false)
11049    }
11050
11051    fn emit_match_ifchain_inner(
11052        &mut self,
11053        scrutinee: &AIRNode,
11054        arms: &[AIRNode],
11055        emit_return: bool,
11056    ) -> Result<(), CodegenError> {
11057        // Single-evaluation root. A bare identifier is already a stable, typed
11058        // reference (emit it through the normal expression path so its name
11059        // matches the rest of the program); anything else is hoisted into a
11060        // typed `__match` local. Either way, leave the cursor indented at the
11061        // chain's column so the leading `if` lines up.
11062        let ind = self.indent_str();
11063        let root: String = if matches!(scrutinee.kind, NodeKind::Identifier { .. }) {
11064            let r = self.expr_to_string(scrutinee)?;
11065            self.write_indent();
11066            r
11067        } else {
11068            let _ = write!(self.buf, "{ind}__match := ");
11069            self.emit_expr(scrutinee)?;
11070            self.buf.push('\n');
11071            self.write_indent();
11072            "__match".to_string()
11073        };
11074
11075        // The scrutinee's declared type, cloned to an owned node so it survives
11076        // the `&mut self` bind emit below. Threads through the pattern lowering
11077        // so a *nested tuple* payload (`Some(Ok((a, b)))`) is asserted to its
11078        // concrete tuple struct rather than read off a bare `interface{}`.
11079        let decl_ty: Option<AIRNode> = self.scrutinee_decl_type_node(scrutinee).cloned();
11080
11081        let arm_count = arms.len();
11082        let mut first = true;
11083        let mut closed = false;
11084        for (idx, arm) in arms.iter().enumerate() {
11085            let NodeKind::MatchArm {
11086                pattern,
11087                guard,
11088                body,
11089            } = &arm.kind
11090            else {
11091                continue;
11092            };
11093            let test = self.pattern_test_go(pattern, &root, decl_ty.as_ref());
11094            let is_catch_all = matches!(
11095                pattern.kind,
11096                NodeKind::WildcardPat | NodeKind::BindPat { .. }
11097            );
11098            let is_last = idx + 1 == arm_count;
11099            let unconditional = guard.is_none() && (is_catch_all || is_last);
11100
11101            if unconditional {
11102                if first {
11103                    self.buf.push('{');
11104                } else {
11105                    self.buf.push_str(" else {");
11106                }
11107                closed = true;
11108            } else {
11109                let mut cond = if test.is_empty() {
11110                    "true".to_string()
11111                } else {
11112                    test
11113                };
11114                if let Some(g) = guard {
11115                    // The guard may reference the arm's pattern bindings; they
11116                    // are only introduced inside the arm body, so evaluate the
11117                    // guard in an anonymous func that re-introduces them. A
11118                    // failed guard then falls through to the next `else if` (the
11119                    // fall-through a `switch` could not express).
11120                    let g_str = self.expr_to_string(g)?;
11121                    let binds =
11122                        self.pattern_binds_to_string_go_typed(pattern, &root, decl_ty.as_ref());
11123                    let guard_test = if binds.is_empty() {
11124                        format!("({g_str})")
11125                    } else {
11126                        format!("func() bool {{ {binds}return ({g_str}) }}()")
11127                    };
11128                    if cond == "true" {
11129                        cond = guard_test;
11130                    } else {
11131                        cond = format!("{cond} && {guard_test}");
11132                    }
11133                }
11134                if first {
11135                    let _ = write!(self.buf, "if {cond} {{");
11136                } else {
11137                    let _ = write!(self.buf, " else if {cond} {{");
11138                }
11139            }
11140            first = false;
11141            self.buf.push('\n');
11142            self.indent += 1;
11143            self.pattern_binds_go_typed(pattern, &root, decl_ty.as_ref())?;
11144            if emit_return {
11145                self.emit_block_body_return(body)?;
11146            } else {
11147                self.emit_block_body(body)?;
11148            }
11149            self.indent -= 1;
11150            self.write_indent();
11151            self.buf.push('}');
11152        }
11153        // A chain with no unconditional arm (all guarded, or no catch-all) needs
11154        // a trailing panic so a value-returning function compiles and an
11155        // unmatched scrutinee fails loudly. Bock matches are exhaustive, so this
11156        // is only ever reached if a guard chain is non-total.
11157        if !closed && !first {
11158            self.buf.push_str(" else {\n");
11159            self.indent += 1;
11160            self.writeln("panic(\"non-exhaustive match\")");
11161            self.indent -= 1;
11162            self.write_indent();
11163            self.buf.push('}');
11164        }
11165        self.buf.push('\n');
11166        Ok(())
11167    }
11168
11169    /// Lower `guard (let PAT = COND) else { … }` (Go has no `let-else`).
11170    ///
11171    /// The pattern's bindings must stay live for the rest of the enclosing block
11172    /// (the whole point of guard-let), and the else arm must diverge (Bock's
11173    /// guard semantics guarantee it — `return`/`break`/`continue`/`panic`). The
11174    /// prior boolean `if !(cond)` lowering both negated a non-bool discriminant
11175    /// (`!(__bockResult{…})` — a `go build` error) and dropped the bound names
11176    /// (`undefined: v`). This emits:
11177    ///
11178    /// ```text
11179    /// __guardN := <cond>
11180    /// if !(<discriminant test>) { <else, diverges> }
11181    /// <name> := <typed payload of __guardN>   // bindings live after the guard
11182    /// ```
11183    ///
11184    /// `Ok`/`Some` payloads are asserted to their concrete element type (recovered
11185    /// from the scrutinee's `Result`/`Optional` element), so typed downstream use
11186    /// (`compare(guess, target)`) compiles; other constructor / record / tuple
11187    /// patterns reuse the shared bind emitter. A bare-bind pattern (always
11188    /// matches) emits only the binding (the else is unreachable).
11189    fn emit_guard_let(
11190        &mut self,
11191        pat: &AIRNode,
11192        condition: &AIRNode,
11193        else_block: &AIRNode,
11194    ) -> Result<(), CodegenError> {
11195        let n = self.guard_counter;
11196        self.guard_counter += 1;
11197        let guard_tmp = format!("__guard{n}");
11198
11199        // Evaluate the discriminant once into a typed local (`:=`), so the test
11200        // and the payload bindings read off one stable value.
11201        let ind = self.indent_str();
11202        let _ = write!(self.buf, "{ind}{guard_tmp} := ");
11203        self.emit_expr(condition)?;
11204        self.buf.push('\n');
11205
11206        // The else arm runs when the pattern does NOT match; it diverges.
11207        let test = self.pattern_test_go(pat, &guard_tmp, None);
11208        if !test.is_empty() {
11209            self.writeln(&format!("if !({test}) {{"));
11210            self.indent += 1;
11211            self.emit_block_body(else_block)?;
11212            self.indent -= 1;
11213            self.writeln("}");
11214        } else {
11215            // A bare-bind/wildcard pattern always matches; bind `__guard` itself
11216            // so it is not "declared and not used", and drop the dead else arm.
11217            self.writeln(&format!("_ = {guard_tmp}"));
11218        }
11219
11220        // Introduce the pattern's bindings into the *enclosing* scope (live after
11221        // the guard). Constructor payloads get a concrete-typed assertion.
11222        self.emit_guard_let_binds(pat, condition, &guard_tmp)?;
11223        Ok(())
11224    }
11225
11226    /// Emit the bindings a guard-let pattern introduces, into the current
11227    /// (enclosing) Go block scope. Reuses the concrete payload typing the
11228    /// Optional/Result match arms apply (`__bockAsInt64`, `.v.(string)`, …) for an
11229    /// `Ok`/`Some` payload bind; everything else falls back to the shared
11230    /// [`Self::pattern_binds_go`] emitter. Each emitted name is recorded in the
11231    /// block's declared-name frame so a later same-name `let` reassigns.
11232    fn emit_guard_let_binds(
11233        &mut self,
11234        pat: &AIRNode,
11235        condition: &AIRNode,
11236        access: &str,
11237    ) -> Result<(), CodegenError> {
11238        if let NodeKind::ConstructorPat { path, fields } = &pat.kind {
11239            let leaf = path.segments.last().map_or("", |s| s.name.as_str());
11240            // Optional/Result single-payload constructor: assert the boxed
11241            // `interface{}` payload to its concrete element type so typed use of
11242            // the bound value compiles.
11243            if matches!(leaf, "Some" | "Ok" | "Err") {
11244                if let Some(f) = fields.first() {
11245                    if let NodeKind::BindPat { name, .. } = &f.kind {
11246                        let bind = go_value_ident(&name.name);
11247                        if bind != "_" {
11248                            let elem_ty = match leaf {
11249                                "Some" => self.scrutinee_optional_elem(condition),
11250                                "Ok" => self.scrutinee_result_elems(condition).map(|(ok, _)| ok),
11251                                _ => self.scrutinee_result_elems(condition).map(|(_, e)| e),
11252                            };
11253                            let rhs = match elem_ty.as_deref() {
11254                                Some("int64") => format!("__bockAsInt64({access}.v)"),
11255                                Some("float64") => format!("__bockAsFloat64({access}.v)"),
11256                                Some(ty) => format!("{access}.v.({ty})"),
11257                                None => format!("{access}.v"),
11258                            };
11259                            self.writeln(&format!("{bind} := {rhs}"));
11260                            self.writeln(&format!("_ = {bind}"));
11261                            self.go_record_declared(&bind);
11262                            return Ok(());
11263                        }
11264                    }
11265                    // Nested payload pattern (`Ok(Ok(v))`, `Some((a, b))`): the
11266                    // payload is re-asserted to its runtime struct where needed
11267                    // and the shared emitter recurses.
11268                    let child = go_typed_access(f, &format!("{access}.v"));
11269                    return self.emit_guard_let_binds_generic(f, &child);
11270                }
11271                return Ok(());
11272            }
11273        }
11274        // User-enum / record / tuple / bind patterns: the shared bind emitter
11275        // already extracts payloads off the asserted variant struct.
11276        self.emit_guard_let_binds_generic(pat, access)
11277    }
11278
11279    /// Fallback guard-let binder: emit `pat`'s bindings off `access` via the
11280    /// shared [`Self::pattern_binds_go`] machinery, then record each bound name in
11281    /// the current Go block frame so a later same-name `let` reassigns.
11282    fn emit_guard_let_binds_generic(
11283        &mut self,
11284        pat: &AIRNode,
11285        access: &str,
11286    ) -> Result<(), CodegenError> {
11287        self.pattern_binds_go(pat, access)?;
11288        let mut binds = String::new();
11289        self.collect_binds_go(pat, access, None, &mut binds);
11290        for stmt in binds.split("; ") {
11291            // Each emitted bind is `name := …`; record the `name`.
11292            if let Some(name) = stmt.split_whitespace().next() {
11293                if name != "_" {
11294                    self.go_record_declared(name);
11295                }
11296            }
11297        }
11298        Ok(())
11299    }
11300
11301    /// Lower a `?` propagation operand (Go has no native `?`).
11302    ///
11303    /// Emits, at statement position, the unwrap-or-early-return prelude for
11304    /// `<inner>?`:
11305    ///
11306    /// ```text
11307    /// __tryN := <inner>
11308    /// if __tryN.tag == "Err" { return __tryN }     // Result: propagate the Err
11309    /// // or, for an Optional operand:
11310    /// if __tryN.tag == "None" { return __bockNone() }
11311    /// ```
11312    ///
11313    /// and returns the Go expression that reads the success payload
11314    /// (`__tryN.v` asserted to the concrete `Ok`/`Some` element type when known).
11315    /// The enclosing function returns a `__bockResult` / `__bockOption`, so an
11316    /// `Err` operand re-propagates directly (`return __tryN`) and a `None` operand
11317    /// returns `__bockNone()`. Bock's type checker guarantees the operand's
11318    /// container and error type are compatible with the enclosing return type, so
11319    /// no zero-value reconstruction of a *different* error type is needed.
11320    ///
11321    /// `?` is only reached in statement-adjacent positions in practice (`let x =
11322    /// e?`, a bare `e?` statement, a tail `e?`); a `?` nested inside a larger
11323    /// expression (`Ok(f()? + 1)`) is not hoisted by this path — the inner emit
11324    /// goes through the normal expression emitter. None of the v1 examples nest
11325    /// `?` that way.
11326    fn emit_try_unwrap(&mut self, inner: &AIRNode) -> Result<String, CodegenError> {
11327        let n = self.try_counter;
11328        self.try_counter += 1;
11329        let tmp = format!("__try{n}");
11330
11331        // Evaluate the operand once.
11332        let ind = self.indent_str();
11333        let _ = write!(self.buf, "{ind}{tmp} := ");
11334        self.emit_expr(inner)?;
11335        self.buf.push('\n');
11336
11337        // Decide Result vs Optional. Both lower to a runtime-tag check, but the
11338        // failure tag (`Err` vs `None`) and propagation value differ. Preference:
11339        //   1. the operand's recoverable `Result`/`Optional` element type;
11340        //   2. otherwise the enclosing function's return container
11341        //      (`__bockResult` → Result, `__bockOption` → Optional);
11342        //   3. default to Result — the overwhelmingly common case, and
11343        //      `return __tryN` is always a valid `__bockResult` to propagate.
11344        let result_elems = self.scrutinee_result_elems(inner);
11345        let opt_elem = self.scrutinee_optional_elem(inner);
11346        let is_optional = if result_elems.is_some() {
11347            false
11348        } else if opt_elem.is_some() {
11349            true
11350        } else {
11351            self.current_fn_ret_type.as_deref() == Some("__bockOption")
11352        };
11353        if is_optional {
11354            // Optional operand: a `None` short-circuits to the enclosing fn's
11355            // `None`. `__bockNone` is the runtime `__bockOption` None value.
11356            self.writeln(&format!("if {tmp}.tag == \"None\" {{"));
11357            self.indent += 1;
11358            self.writeln("return __bockNone");
11359            self.indent -= 1;
11360            self.writeln("}");
11361            Ok(self.try_payload_access(&tmp, opt_elem.as_deref()))
11362        } else {
11363            self.writeln(&format!("if {tmp}.tag == \"Err\" {{"));
11364            self.indent += 1;
11365            self.writeln(&format!("return {tmp}"));
11366            self.indent -= 1;
11367            self.writeln("}");
11368            let ok_ty = result_elems.map(|(ok, _)| ok);
11369            Ok(self.try_payload_access(&tmp, ok_ty.as_deref()))
11370        }
11371    }
11372
11373    /// The Go expression that reads a `?`-unwrapped payload from `tmp.v`, asserted
11374    /// to the concrete element type `elem` when known. Numeric payloads use the
11375    /// widening helpers (an untyped Go constant boxed as `int`/`float64` would
11376    /// panic on a hard `.(int64)` assertion); everything else uses a direct type
11377    /// assertion, falling back to the bare `interface{}` payload when the element
11378    /// type is not statically recoverable.
11379    fn try_payload_access(&self, tmp: &str, elem: Option<&str>) -> String {
11380        match elem {
11381            Some("int64") => format!("__bockAsInt64({tmp}.v)"),
11382            Some("float64") => format!("__bockAsFloat64({tmp}.v)"),
11383            // A `Void`/unit payload (`Result[Void, _]` → `Ok(())` boxes `nil`)
11384            // and the `interface{}` fallback both read the raw payload — a hard
11385            // `.(struct{})` / `.(interface{})` assertion on a boxed `nil` panics.
11386            Some("struct{}") | Some("interface{}") | Some("any") | None => format!("{tmp}.v"),
11387            Some(ty) => format!("{tmp}.v.({ty})"),
11388        }
11389    }
11390
11391    /// The Go expression reading an Optional/Result *leaf* payload bind off the
11392    /// runtime container `access` (`access.v`), asserted to the concrete element
11393    /// `elem` when known. The pattern-match analogue of [`Self::try_payload_access`]
11394    /// (which takes a bare temp name): a `match v { Some(n) if (n > 0) => … }`
11395    /// binds `n := access.v.(int64)` so typed use of `n` compiles. Numeric
11396    /// payloads use the widening helpers (a boxed untyped Go constant would panic
11397    /// on a hard `.(int64)`); `Void`/unit/`interface{}` and the unknown-type
11398    /// fallback read the raw payload (a hard assertion on a boxed `nil` panics).
11399    fn payload_access_go(&self, access: &str, elem: Option<&str>) -> String {
11400        match elem {
11401            Some("int64") => format!("__bockAsInt64({access}.v)"),
11402            Some("float64") => format!("__bockAsFloat64({access}.v)"),
11403            Some("struct{}") | Some("interface{}") | Some("any") | None => {
11404                format!("{access}.v")
11405            }
11406            Some(ty) => format!("{access}.v.({ty})"),
11407        }
11408    }
11409
11410    /// Peel one declared-type layer for an Optional/Result constructor tag,
11411    /// returning the inner type node carried by the matched payload: `Some`/`None`
11412    /// peel `Optional[T]` → `T`; `Ok` peel `Result[T, E]` → `T`; `Err` → `E`.
11413    /// Returns `None` when the declared type is unknown or does not match the
11414    /// tag's container (the payload then stays the runtime `interface{}` — never
11415    /// wrong, only un-asserted). The result is `cloned` so the borrow of
11416    /// `decl_ty` does not outlive the recursion step.
11417    fn peel_constructor_decl_ty(
11418        &self,
11419        leaf: &str,
11420        decl_ty: Option<&AIRNode>,
11421    ) -> Option<Box<AIRNode>> {
11422        let ty = decl_ty?;
11423        match leaf {
11424            "Some" | "None" => self
11425                .optional_inner_type_node(ty)
11426                .map(|n| Box::new(n.clone())),
11427            "Ok" => self
11428                .result_inner_type_nodes(ty)
11429                .map(|(ok, _)| Box::new(ok.clone())),
11430            "Err" => self
11431                .result_inner_type_nodes(ty)
11432                .and_then(|(_, err)| err)
11433                .map(|n| Box::new(n.clone())),
11434            _ => None,
11435        }
11436    }
11437
11438    /// The Go access expression for the payload of an Optional/Result
11439    /// constructor pattern's sole field. `Some(Ok(…))` re-asserts the boxed
11440    /// `interface{}` payload to the inner container runtime struct
11441    /// (`.v.(__bockResult)`), via [`go_typed_access`]. A nested *tuple* payload
11442    /// (`Some(Ok((a, b)))`) instead asserts the payload to its concrete tuple
11443    /// struct (`.v.(struct{ Field0 int64; Field1 int64 })`) so the subsequent
11444    /// `.Field0`/`.Field1` reads type-check — without it the field access lands
11445    /// on a bare `interface{}` and fails `go build`. `child_ty` is the
11446    /// peeled declared type of the payload (the tuple type, when known).
11447    fn constructor_child_access_go(
11448        &self,
11449        child: &AIRNode,
11450        access: &str,
11451        child_ty: Option<&AIRNode>,
11452    ) -> String {
11453        if let NodeKind::TuplePat { .. } = &child.kind {
11454            if let Some(t) = child_ty {
11455                if let NodeKind::TypeTuple { .. } = &t.kind {
11456                    let struct_ty = self.type_to_go(t);
11457                    return format!("{access}.v.({struct_ty})");
11458                }
11459            }
11460        }
11461        go_typed_access(child, &format!("{access}.v"))
11462    }
11463
11464    /// The per-field declared type nodes of a tuple pattern position, recovered
11465    /// from a declared `TypeTuple` of the expected arity. Returns a vector of
11466    /// `Option<&AIRNode>` (one per field, `None` where the declared type is
11467    /// unknown or not a matching tuple) so the caller can thread each field's
11468    /// type into the element sub-pattern.
11469    fn tuple_field_decl_tys<'a>(
11470        &self,
11471        decl_ty: Option<&'a AIRNode>,
11472        arity: usize,
11473    ) -> Vec<Option<&'a AIRNode>> {
11474        if let Some(t) = decl_ty {
11475            if let NodeKind::TypeTuple { elems } = &t.kind {
11476                if elems.len() == arity {
11477                    return elems.iter().map(Some).collect();
11478                }
11479            }
11480        }
11481        vec![None; arity]
11482    }
11483
11484    /// Build the boolean test that selects `pat` against the Go expression
11485    /// `access` (a correctly-typed value at this pattern position). Returns the
11486    /// empty string for a pattern that always matches (wildcard / bare bind).
11487    ///
11488    /// `decl_ty`, when present, is the declared type-expression node of the
11489    /// value at this position (recovered from the match scrutinee's declared
11490    /// type and peeled as the recursion descends `Some`/`Ok`/`Err`). It is only
11491    /// needed to type-assert a *nested tuple* payload (`Some(Ok((a, b)))`) to its
11492    /// concrete tuple struct before reading `.Field0`; every other arm ignores
11493    /// it and passes `None` down (the value at that position is already concrete).
11494    fn pattern_test_go(&self, pat: &AIRNode, access: &str, decl_ty: Option<&AIRNode>) -> String {
11495        match &pat.kind {
11496            NodeKind::WildcardPat | NodeKind::BindPat { .. } => String::new(),
11497            NodeKind::LiteralPat { lit } => {
11498                format!("{access} == {}", go_literal(lit))
11499            }
11500            NodeKind::ConstructorPat { path, fields } => {
11501                let leaf = path.segments.last().map_or("", |s| s.name.as_str());
11502                // Optional / Result dispatch on the runtime `.tag`; the payload
11503                // is `<access>.v` (an `interface{}` the child must re-assert).
11504                if matches!(leaf, "Some" | "None" | "Ok" | "Err") {
11505                    let mut tests = vec![format!("{access}.tag == \"{leaf}\"")];
11506                    if let Some(f) = fields.first() {
11507                        // Peel one declared-type layer matching this tag so a
11508                        // nested tuple payload can be asserted to its struct.
11509                        let child_ty = self.peel_constructor_decl_ty(leaf, decl_ty);
11510                        let child =
11511                            self.constructor_child_access_go(f, access, child_ty.as_deref());
11512                        let sub = self.pattern_test_go(f, &child, child_ty.as_deref());
11513                        if !sub.is_empty() {
11514                            tests.push(sub);
11515                        }
11516                    }
11517                    return tests.join(" && ");
11518                }
11519                // User enum: a sealed-interface value; test via a comma-ok type
11520                // assertion to the concrete variant struct.
11521                let variant_ty = self.go_variant_struct(path);
11522                format!("func() bool {{ _, ok := {access}.({variant_ty}); return ok }}()")
11523            }
11524            NodeKind::RecordPat { path, fields, .. } => {
11525                if self.user_variant_for_path(path).is_some() {
11526                    let variant_ty = self.go_variant_struct(path);
11527                    // Field sub-tests would require binding the asserted struct;
11528                    // a struct-variant record pattern with nested field patterns
11529                    // is rare — test the variant type and let binds extract.
11530                    let _ = fields;
11531                    return format!(
11532                        "func() bool {{ _, ok := {access}.({variant_ty}); return ok }}()"
11533                    );
11534                }
11535                // A *plain* record (`Point { x: 0, y }`) is already the concrete
11536                // struct, so test its field sub-patterns directly off
11537                // `access.<Field>` (a literal field constrains the arm; a bind /
11538                // wildcard field adds no test). Without these tests every plain-
11539                // record arm matched unconditionally, so `Point { x: 0, y: 0 }`
11540                // shadowed every later `Point { … }` arm.
11541                let mut tests = Vec::new();
11542                for f in fields {
11543                    if let Some(p) = &f.pattern {
11544                        let go_field = to_pascal_case(&f.name.name);
11545                        let sub = self.pattern_test_go(p, &format!("{access}.{go_field}"), None);
11546                        if !sub.is_empty() {
11547                            tests.push(sub);
11548                        }
11549                    }
11550                }
11551                if tests.is_empty() {
11552                    String::new()
11553                } else {
11554                    tests.join(" && ")
11555                }
11556            }
11557            NodeKind::TuplePat { elems } => {
11558                // The access is already the concrete tuple struct (the parent
11559                // `Some`/`Ok` peel asserted it). Per-field types come from the
11560                // declared tuple type when known.
11561                let field_tys = self.tuple_field_decl_tys(decl_ty, elems.len());
11562                let mut tests = Vec::new();
11563                for (i, e) in elems.iter().enumerate() {
11564                    let sub = self.pattern_test_go(
11565                        e,
11566                        &format!("{access}.Field{i}"),
11567                        field_tys.get(i).and_then(|t| *t),
11568                    );
11569                    if !sub.is_empty() {
11570                        tests.push(sub);
11571                    }
11572                }
11573                if tests.is_empty() {
11574                    String::new()
11575                } else {
11576                    tests.join(" && ")
11577                }
11578            }
11579            NodeKind::ListPat { elems, rest } => {
11580                // Bock lists are Go slices (`[]T`). `[a, b]` requires an exact
11581                // length; `[a, ..rest]` requires at least len(elems). Element
11582                // sub-patterns are tested positionally (`access[i]`); the rest
11583                // binds the tail slice and adds no test. Mirrors `pattern_test_js`.
11584                let n = elems.len();
11585                let len_test = if rest.is_some() {
11586                    format!("len({access}) >= {n}")
11587                } else {
11588                    format!("len({access}) == {n}")
11589                };
11590                let mut tests = vec![len_test];
11591                for (i, e) in elems.iter().enumerate() {
11592                    let sub = self.pattern_test_go(e, &format!("{access}[{i}]"), None);
11593                    if !sub.is_empty() {
11594                        tests.push(sub);
11595                    }
11596                }
11597                tests.join(" && ")
11598            }
11599            NodeKind::RangePat { lo, hi, inclusive } => {
11600                // `lo..hi` → `access >= lo && access < hi`; `lo..=hi` uses `<=`.
11601                // Mirrors `pattern_test_js`.
11602                let lo_s = range_bound_to_go(lo);
11603                let hi_s = range_bound_to_go(hi);
11604                let upper = if *inclusive { "<=" } else { "<" };
11605                format!("{access} >= {lo_s} && {access} {upper} {hi_s}")
11606            }
11607            NodeKind::OrPat { alternatives } => {
11608                let alts: Vec<String> = alternatives
11609                    .iter()
11610                    .map(|a| {
11611                        let t = self.pattern_test_go(a, access, decl_ty);
11612                        if t.is_empty() {
11613                            "true".to_string()
11614                        } else {
11615                            format!("({t})")
11616                        }
11617                    })
11618                    .collect();
11619                alts.join(" || ")
11620            }
11621            _ => String::new(),
11622        }
11623    }
11624
11625    /// Emit the `name := <access…>; _ = name` bindings introduced by `pat`,
11626    /// recursing into nested constructor / record / tuple sub-patterns. The
11627    /// trailing `_ = name` keeps an unused binding from failing `go build`.
11628    fn pattern_binds_go(&mut self, pat: &AIRNode, access: &str) -> Result<(), CodegenError> {
11629        self.pattern_binds_go_typed(pat, access, None)
11630    }
11631
11632    /// As [`Self::pattern_binds_go`], but threading the scrutinee's declared type
11633    /// node so a nested tuple payload is bound off a struct-asserted access.
11634    fn pattern_binds_go_typed(
11635        &mut self,
11636        pat: &AIRNode,
11637        access: &str,
11638        decl_ty: Option<&AIRNode>,
11639    ) -> Result<(), CodegenError> {
11640        let binds = self.pattern_binds_to_string_go_typed(pat, access, decl_ty);
11641        if binds.is_empty() {
11642            return Ok(());
11643        }
11644        // `pattern_binds_to_string_go` emits each `name := …; _ = name; `
11645        // separated by `; `; split onto its own indented line for readability.
11646        for stmt in binds.split("; ") {
11647            let stmt = stmt.trim();
11648            if stmt.is_empty() {
11649                continue;
11650            }
11651            self.writeln(stmt);
11652        }
11653        Ok(())
11654    }
11655
11656    /// Collect `pat`'s bindings as a single-line string of `name := …; _ = name;
11657    /// ` statements. Shared by [`Self::pattern_binds_go`] (statement position)
11658    /// and the guard-evaluating anonymous func in [`Self::emit_match_ifchain`].
11659    /// Threads the scrutinee's declared type node so a nested tuple payload
11660    /// (`Some(Ok((a, b)))`) is bound off a struct-asserted access rather than a
11661    /// bare `interface{}`.
11662    fn pattern_binds_to_string_go_typed(
11663        &self,
11664        pat: &AIRNode,
11665        access: &str,
11666        decl_ty: Option<&AIRNode>,
11667    ) -> String {
11668        let mut out = String::new();
11669        self.collect_binds_go(pat, access, decl_ty, &mut out);
11670        out
11671    }
11672
11673    fn collect_binds_go(
11674        &self,
11675        pat: &AIRNode,
11676        access: &str,
11677        decl_ty: Option<&AIRNode>,
11678        out: &mut String,
11679    ) {
11680        match &pat.kind {
11681            NodeKind::BindPat { name, .. } => {
11682                let n = go_value_ident(&name.name);
11683                let _ = write!(out, "{n} := {access}; _ = {n}; ");
11684            }
11685            NodeKind::ConstructorPat { path, fields } => {
11686                let leaf = path.segments.last().map_or("", |s| s.name.as_str());
11687                if matches!(leaf, "Some" | "None" | "Ok" | "Err") {
11688                    if let Some(f) = fields.first() {
11689                        // Peel one declared-type layer for this tag so a nested
11690                        // tuple payload is asserted to its concrete struct before
11691                        // its `.Field0`/`.Field1` reads (mirrors `pattern_test_go`).
11692                        let child_ty = self.peel_constructor_decl_ty(leaf, decl_ty);
11693                        // A *leaf* payload bind (`Some(n)`, `Ok(v)`) with a known
11694                        // concrete element type asserts the boxed `interface{}`
11695                        // payload to that type, so typed use inside a guard or
11696                        // arm body (`n > 0`) type-checks — the same payload typing
11697                        // the guard-let binder and the tag-switch arms apply. A
11698                        // nested constructor/tuple/record payload re-asserts via
11699                        // `constructor_child_access_go` and recurses.
11700                        if let NodeKind::BindPat { name, .. } = &f.kind {
11701                            let n = go_value_ident(&name.name);
11702                            if n != "_" {
11703                                let elem = child_ty.as_deref().map(|t| self.type_to_go(t));
11704                                let rhs = self.payload_access_go(access, elem.as_deref());
11705                                let _ = write!(out, "{n} := {rhs}; _ = {n}; ");
11706                                return;
11707                            }
11708                        }
11709                        let child =
11710                            self.constructor_child_access_go(f, access, child_ty.as_deref());
11711                        self.collect_binds_go(f, &child, child_ty.as_deref(), out);
11712                    }
11713                } else {
11714                    // User-enum variant: bind payload fields off the asserted
11715                    // concrete struct.
11716                    let variant_ty = self.go_variant_struct(path);
11717                    for (i, f) in fields.iter().enumerate() {
11718                        let child = format!("{access}.({variant_ty}).Field{i}");
11719                        self.collect_binds_go(f, &child, None, out);
11720                    }
11721                }
11722            }
11723            NodeKind::RecordPat { path, fields, .. } => {
11724                // A registered enum *variant* record (`Shape::Rect { w, h }`) is a
11725                // sealed-interface value, so its fields are read off a concrete
11726                // type assertion (`access.(ShapeRect).W`). A *plain* record
11727                // (`Point { x, y }`) is already a concrete struct — asserting
11728                // `access.(Point)` is invalid Go ("p is not an interface"), so its
11729                // fields are read directly (`access.X`). Mirrors the
11730                // `user_variant_for_path` gate in `pattern_test_go`.
11731                let base = if self.user_variant_for_path(path).is_some() {
11732                    let variant_ty = self.go_variant_struct(path);
11733                    format!("{access}.({variant_ty})")
11734                } else {
11735                    access.to_string()
11736                };
11737                for f in fields {
11738                    let go_field = to_pascal_case(&f.name.name);
11739                    let child = format!("{base}.{go_field}");
11740                    match &f.pattern {
11741                        Some(p) => self.collect_binds_go(p, &child, None, out),
11742                        None => {
11743                            let n = to_camel_case(&f.name.name);
11744                            let _ = write!(out, "{n} := {child}; _ = {n}; ");
11745                        }
11746                    }
11747                }
11748            }
11749            NodeKind::TuplePat { elems } => {
11750                let field_tys = self.tuple_field_decl_tys(decl_ty, elems.len());
11751                for (i, e) in elems.iter().enumerate() {
11752                    self.collect_binds_go(
11753                        e,
11754                        &format!("{access}.Field{i}"),
11755                        field_tys.get(i).and_then(|t| *t),
11756                        out,
11757                    );
11758                }
11759            }
11760            NodeKind::ListPat { elems, rest } => {
11761                for (i, e) in elems.iter().enumerate() {
11762                    self.collect_binds_go(e, &format!("{access}[{i}]"), None, out);
11763                }
11764                // `..rest` binds the remaining elements as a tail slice
11765                // (`rest := access[n:]`); a bare `..` (RestPat) or absent rest
11766                // binds nothing. Mirrors `pattern_binds_js`.
11767                if let Some(r) = rest {
11768                    if let NodeKind::BindPat { name, .. } = &r.kind {
11769                        let nm = go_value_ident(&name.name);
11770                        let _ = write!(out, "{nm} := {access}[{}:]; _ = {nm}; ", elems.len());
11771                    }
11772                }
11773            }
11774            NodeKind::OrPat { alternatives } => {
11775                if let Some(first) = alternatives.first() {
11776                    self.collect_binds_go(first, access, decl_ty, out);
11777                }
11778            }
11779            _ => {}
11780        }
11781    }
11782
11783    /// The Go struct type name for a user-enum variant path (`ShapeRect`), or the
11784    /// joined path as a fallback.
11785    fn go_variant_struct(&self, path: &bock_ast::TypePath) -> String {
11786        if let Some(info) = self.user_variant_for_path(path) {
11787            let variant = path.segments.last().map_or("", |s| s.name.as_str());
11788            format!("{}{variant}", info.enum_name)
11789        } else {
11790            path.segments
11791                .iter()
11792                .map(|s| s.name.as_str())
11793                .collect::<Vec<_>>()
11794                .join("")
11795        }
11796    }
11797
11798    fn emit_match_arm(
11799        &mut self,
11800        arm: &AIRNode,
11801        user_enum: bool,
11802        value_switch_binds: bool,
11803    ) -> Result<(), CodegenError> {
11804        if let NodeKind::MatchArm {
11805            pattern,
11806            guard,
11807            body,
11808        } = &arm.kind
11809        {
11810            let ind = self.indent_str();
11811            match &pattern.kind {
11812                NodeKind::WildcardPat | NodeKind::BindPat { .. } => {
11813                    let _ = write!(self.buf, "{ind}default:");
11814                }
11815                _ => {
11816                    let _ = write!(self.buf, "{ind}case ");
11817                    self.emit_match_case_condition(pattern)?;
11818                    self.buf.push(':');
11819                }
11820            }
11821            self.buf.push('\n');
11822            self.indent += 1;
11823            // For a user enum type-switch, bind the arm's payload fields from
11824            // the concrete `__v` (`radius := __v.Radius`, `w := __v.Field0`).
11825            if user_enum {
11826                self.emit_user_enum_arm_bindings(pattern)?;
11827            }
11828            // Value-switch bind arm (`x => …`): alias the named scrutinee `__v`
11829            // so the body's references resolve (the Go binding-drop fix).
11830            if value_switch_binds {
11831                if let NodeKind::BindPat { name, .. } = &pattern.kind {
11832                    let n = go_value_ident(&name.name);
11833                    self.writeln(&format!("{n} := __v; _ = {n}"));
11834                }
11835            }
11836            if let Some(g) = guard {
11837                let gi = self.indent_str();
11838                let _ = write!(self.buf, "{gi}if ");
11839                self.emit_expr(g)?;
11840                self.buf.push_str(" {\n");
11841                self.indent += 1;
11842                self.emit_block_body(body)?;
11843                self.indent -= 1;
11844                self.writeln("}");
11845            } else {
11846                self.emit_block_body(body)?;
11847            }
11848        }
11849        Ok(())
11850    }
11851
11852    /// Bind a user-enum arm's payload fields from the type-switched `__v`.
11853    ///
11854    /// Inside a Go type-switch case `case ShapeCircle:`, `__v` has the concrete
11855    /// variant-struct type, so each bound field reads directly off it:
11856    /// - struct variant (`Circle { radius }`): `radius := __v.Radius`
11857    ///   (the struct field is `to_pascal_case` of the Bock field name).
11858    /// - tuple variant (`Rect(w, h)`): `w := __v.Field0; h := __v.Field1`.
11859    /// - unit variant: nothing to bind.
11860    ///
11861    /// Each binding is followed by `_ = name` so an arm that does not use every
11862    /// payload field still compiles (Go errors on an unused local).
11863    fn emit_user_enum_arm_bindings(&mut self, pattern: &AIRNode) -> Result<(), CodegenError> {
11864        match &pattern.kind {
11865            NodeKind::ConstructorPat { fields, .. } => {
11866                for (i, field) in fields.iter().enumerate() {
11867                    let name = self.pattern_to_binding_name(field);
11868                    if name == "_" {
11869                        continue;
11870                    }
11871                    self.writeln(&format!("{name} := __v.Field{i}; _ = {name}"));
11872                }
11873            }
11874            NodeKind::RecordPat { fields, .. } => {
11875                for f in fields {
11876                    let go_field = to_pascal_case(&f.name.name);
11877                    let bind = match &f.pattern {
11878                        Some(p) => self.pattern_to_binding_name(p),
11879                        // Shorthand `{ radius }` binds a variable named `radius`.
11880                        None => to_camel_case(&f.name.name),
11881                    };
11882                    if bind == "_" {
11883                        continue;
11884                    }
11885                    self.writeln(&format!("{bind} := __v.{go_field}; _ = {bind}"));
11886                }
11887            }
11888            _ => {}
11889        }
11890        Ok(())
11891    }
11892
11893    fn emit_match_case_condition(&mut self, pat: &AIRNode) -> Result<(), CodegenError> {
11894        match &pat.kind {
11895            NodeKind::WildcardPat => {
11896                self.buf.push('_');
11897            }
11898            NodeKind::BindPat { name, .. } => {
11899                let _ = name;
11900                self.buf.push_str("interface{}");
11901            }
11902            NodeKind::LiteralPat { lit } => match lit {
11903                Literal::Int(s) => self.buf.push_str(s),
11904                Literal::Float(s) => self.buf.push_str(s),
11905                Literal::Bool(b) => self.buf.push_str(if *b { "true" } else { "false" }),
11906                Literal::Char(s) => {
11907                    self.buf.push('\'');
11908                    self.buf.push_str(s);
11909                    self.buf.push('\'');
11910                }
11911                Literal::String(s) => {
11912                    self.buf.push('"');
11913                    self.buf.push_str(&escape_go_string(s));
11914                    self.buf.push('"');
11915                }
11916                Literal::Unit => self.buf.push_str("nil"),
11917            },
11918            NodeKind::ConstructorPat { path, .. } => {
11919                // A user enum variant is a `{enum}{variant}` struct type
11920                // (`ShapeRect`); fall back to the joined path otherwise. For a
11921                // generic enum the variant struct carries the enum's type params,
11922                // so a `case BoxFull[int64]:` spells the concrete instantiation
11923                // (`current_match_enum_type_args`, empty for a non-generic enum).
11924                let variant_name = if let Some(info) = self.user_variant_for_path(path) {
11925                    let variant = path.segments.last().map_or("", |s| s.name.as_str());
11926                    format!(
11927                        "{}{variant}{}",
11928                        info.enum_name, self.current_match_enum_type_args
11929                    )
11930                } else {
11931                    path.segments
11932                        .iter()
11933                        .map(|s| s.name.as_str())
11934                        .collect::<Vec<_>>()
11935                        .join("")
11936                };
11937                self.buf.push_str(&variant_name);
11938            }
11939            NodeKind::RecordPat { path, .. } => {
11940                let type_name = if let Some(info) = self.user_variant_for_path(path) {
11941                    let variant = path.segments.last().map_or("", |s| s.name.as_str());
11942                    format!(
11943                        "{}{variant}{}",
11944                        info.enum_name, self.current_match_enum_type_args
11945                    )
11946                } else {
11947                    path.segments
11948                        .iter()
11949                        .map(|s| s.name.as_str())
11950                        .collect::<Vec<_>>()
11951                        .join(".")
11952                };
11953                self.buf.push_str(&type_name);
11954            }
11955            NodeKind::TuplePat { .. } => {
11956                self.buf.push_str("interface{}");
11957            }
11958            _ => {
11959                self.buf.push_str("interface{}");
11960            }
11961        }
11962        Ok(())
11963    }
11964
11965    // ── Pipe operator ───────────────────────────────────────────────────────
11966
11967    fn emit_pipe(&mut self, left: &AIRNode, right: &AIRNode) -> Result<(), CodegenError> {
11968        if let NodeKind::Call { callee, args, .. } = &right.kind {
11969            let has_placeholder = args
11970                .iter()
11971                .any(|a| matches!(a.value.kind, NodeKind::Placeholder));
11972            if has_placeholder {
11973                self.emit_expr(callee)?;
11974                self.buf.push('(');
11975                for (i, arg) in args.iter().enumerate() {
11976                    if i > 0 {
11977                        self.buf.push_str(", ");
11978                    }
11979                    if matches!(arg.value.kind, NodeKind::Placeholder) {
11980                        self.emit_expr(left)?;
11981                    } else {
11982                        self.emit_expr(&arg.value)?;
11983                    }
11984                }
11985                self.buf.push(')');
11986                return Ok(());
11987            }
11988        }
11989        self.emit_expr(right)?;
11990        self.buf.push('(');
11991        self.emit_expr(left)?;
11992        self.buf.push(')');
11993        Ok(())
11994    }
11995
11996    // ── Type emission ───────────────────────────────────────────────────────
11997
11998    /// If `name` is one of the three collection types, emit the concrete Go
11999    /// container type with its element/key/value types recovered from `args`
12000    /// (each mapped to Go via `arg_to_go`), rather than the `interface{}`-erased
12001    /// `map_type_name` fallback:
12002    /// - `List[T]`  → `[]T`
12003    /// - `Set[T]`   → `map[T]struct{}`
12004    /// - `Map[K,V]` → `map[K]V`
12005    ///
12006    /// A missing arg defaults to `interface{}` (e.g. a bare `List` with no type
12007    /// argument), preserving the prior erased behavior for the untyped case.
12008    /// Returns `None` for any non-collection type so callers fall through to the
12009    /// `Optional`/`Result` runtime-struct and generic-struct paths unchanged.
12010    fn collection_type_to_go<T>(
12011        &self,
12012        name: &str,
12013        args: &[T],
12014        arg_to_go: impl Fn(&T) -> String,
12015    ) -> Option<String> {
12016        let elem = |i: usize| {
12017            args.get(i)
12018                .map_or_else(|| "interface{}".to_string(), &arg_to_go)
12019        };
12020        match name {
12021            "List" => Some(format!("[]{}", elem(0))),
12022            "Set" => Some(format!("map[{}]struct{{}}", elem(0))),
12023            "Map" => Some(format!("map[{}]{}", elem(0), elem(1))),
12024            _ => None,
12025        }
12026    }
12027
12028    fn type_to_go(&self, node: &AIRNode) -> String {
12029        match &node.kind {
12030            NodeKind::TypeNamed { path, args } => {
12031                // A non-generic `type X = …` alias renders as its underlying Go
12032                // type (`type ParseResult = Result[...]` → `__bockResult`), so a
12033                // value of the alias type is the runtime container a `match`
12034                // dispatches on. Resolved before the collection/runtime mapping so
12035                // an alias to `List[T]`/`Result`/`Optional` lowers correctly.
12036                if let Some(target) = self.resolve_type_alias(node) {
12037                    return self.type_to_go(target);
12038                }
12039                let name = path
12040                    .segments
12041                    .iter()
12042                    .map(|s| s.name.as_str())
12043                    .collect::<Vec<_>>()
12044                    .join(".");
12045                // The three collection types are NOT erased to an `interface{}`
12046                // element: a declared `List[Int]` must emit `[]int64` (not
12047                // `[]interface{}`) so element arithmetic / iteration / typed
12048                // returns compile. Emit the concrete Go container recursively
12049                // over the type args, BEFORE the `map_type_name`
12050                // `is_mapped_runtime` fallback (which would erase them).
12051                if let Some(collection) =
12052                    self.collection_type_to_go(&name, args, |a| self.type_to_go(a))
12053                {
12054                    return collection;
12055                }
12056                let go_name = self.map_type_name(&name);
12057                // Runtime container types (`__bockOption`, `__bockResult`) carry
12058                // their payload as `interface{}`, not as a Go generic parameter;
12059                // never append `[T]` to such a mapped runtime type.
12060                let is_mapped_runtime = go_name != name;
12061                if args.is_empty() || is_mapped_runtime {
12062                    go_name
12063                } else {
12064                    let arg_strs: Vec<String> = args.iter().map(|a| self.type_to_go(a)).collect();
12065                    format!("{go_name}[{}]", arg_strs.join(", "))
12066                }
12067            }
12068            NodeKind::TypeTuple { elems } => {
12069                // Go doesn't have tuples; emit as struct with numbered fields.
12070                if elems.is_empty() {
12071                    "struct{}".into()
12072                } else {
12073                    let fields: Vec<String> = elems
12074                        .iter()
12075                        .enumerate()
12076                        .map(|(i, e)| format!("Field{i} {}", self.type_to_go(e)))
12077                        .collect();
12078                    format!("struct{{ {} }}", fields.join("; "))
12079                }
12080            }
12081            NodeKind::TypeFunction { params, ret, .. } => {
12082                let param_strs: Vec<String> = params.iter().map(|p| self.type_to_go(p)).collect();
12083                // A `Fn(...) -> Void` lowers to a Go `func(...)` with NO result
12084                // type. Rendering the Void return as `struct{}` (its value type)
12085                // would make `func() struct{}`, which is a function that must
12086                // `return struct{}{}` — but a Void-returning closure body emits
12087                // no return, so the signatures would not match. Drop the result.
12088                if Self::is_void_type(ret) {
12089                    format!("func({})", param_strs.join(", "))
12090                } else {
12091                    format!("func({}) {}", param_strs.join(", "), self.type_to_go(ret))
12092                }
12093            }
12094            NodeKind::TypeOptional { inner } => {
12095                // `T?` lowers to the tagged Optional runtime struct, not a Go
12096                // pointer — pointers can\'t represent `Some(nil-able-T)` vs
12097                // `None`, and the match dispatches on the tag.
12098                let _ = inner;
12099                "__bockOption".to_string()
12100            }
12101            NodeKind::TypeSelf => self
12102                .go_self_subst
12103                .clone()
12104                .unwrap_or_else(|| "/* Self */".into()),
12105            _ => "interface{}".into(),
12106        }
12107    }
12108
12109    fn map_type_name(&self, name: &str) -> String {
12110        match name {
12111            "Int" => "int64".into(),
12112            "Float" => "float64".into(),
12113            "Bool" => "bool".into(),
12114            "String" => "string".into(),
12115            // Bock `Char` is a Unicode scalar; Go's `rune` (`int32`). A char
12116            // literal `'A'` already emits a Go rune literal (`go_literal`), so a
12117            // `let c: Char` annotation must render `rune`, not the undefined
12118            // identifier `Char`.
12119            "Char" => "rune".into(),
12120            "Void" | "Unit" => "struct{}".into(),
12121            "List" => "[]interface{}".into(),
12122            "Map" => "map[string]interface{}".into(),
12123            "Set" => "map[interface{}]struct{}".into(),
12124            "Any" => "interface{}".into(),
12125            "Never" => "interface{}".into(),
12126            "Channel" => "*__bockChannel".into(),
12127            "Optional" => "__bockOption".into(),
12128            // `Result[T, E]` lowers to the tagged Result-runtime struct (the
12129            // `[T, E]` args are dropped — `is_mapped_runtime` in the callers
12130            // suppresses the generic suffix), mirroring `Optional`.
12131            "Result" => "__bockResult".into(),
12132            // §18.3.1 builtin time types: a `Duration` value lowers to a
12133            // signed-nanosecond `int64`, and an `Instant` to `time.Time`
12134            // (`time.Now()`). They are NOT user-defined types, so as annotations
12135            // (e.g. on a `Clock` handler's `now_monotonic() -> Instant` /
12136            // `sleep(duration: Duration)`) they must render their concrete Go
12137            // forms, not the undefined identifiers. (The `time` import is driven
12138            // by the value sites — `time.Now()` / `time.Since(...)` — that any
12139            // `Instant`-typed program also exercises.)
12140            "Duration" => "int64".into(),
12141            "Instant" => "time.Time".into(),
12142            // The prelude `Ordering` enum: when the real `core.compare.Ordering`
12143            // is NOT reachable (no `use core.compare`), its variants lower to the
12144            // `__bockOrdering` value runtime, so a `-> Ordering` annotation must
12145            // render `__bockOrdering` to agree — the bare `Ordering` is an
12146            // undefined Go type. When the enum IS reachable the user type is in
12147            // scope and keeps its name. (Q-prelude-impl-missing-import.)
12148            "Ordering" if !self.ordering_enum_reachable() => "__bockOrdering".into(),
12149            other => other.into(),
12150        }
12151    }
12152
12153    fn ast_type_to_go(&self, ty: &TypeExpr) -> String {
12154        match ty {
12155            TypeExpr::Named { path, args, .. } => {
12156                let name = path
12157                    .segments
12158                    .iter()
12159                    .map(|s| s.name.as_str())
12160                    .collect::<Vec<_>>()
12161                    .join(".");
12162                // See `type_to_go`: emit concrete `[]T` / `map[K]V` /
12163                // `map[T]struct{}` for the three collection types rather than
12164                // erasing their element type to `interface{}`.
12165                if let Some(collection) =
12166                    self.collection_type_to_go(&name, args, |a| self.ast_type_to_go(a))
12167                {
12168                    return collection;
12169                }
12170                let go_name = self.map_type_name(&name);
12171                let is_mapped_runtime = go_name != name;
12172                if args.is_empty() || is_mapped_runtime {
12173                    go_name
12174                } else {
12175                    let arg_strs: Vec<String> =
12176                        args.iter().map(|a| self.ast_type_to_go(a)).collect();
12177                    format!("{go_name}[{}]", arg_strs.join(", "))
12178                }
12179            }
12180            TypeExpr::Tuple { elems, .. } => {
12181                if elems.is_empty() {
12182                    "struct{}".into()
12183                } else {
12184                    let fields: Vec<String> = elems
12185                        .iter()
12186                        .enumerate()
12187                        .map(|(i, e)| format!("Field{i} {}", self.ast_type_to_go(e)))
12188                        .collect();
12189                    format!("struct{{ {} }}", fields.join("; "))
12190                }
12191            }
12192            TypeExpr::Function { params, ret, .. } => {
12193                let param_strs: Vec<String> =
12194                    params.iter().map(|p| self.ast_type_to_go(p)).collect();
12195                // `Fn(...) -> Void` → Go `func(...)` (no result type). See the
12196                // AIR `TypeFunction` arm in `type_to_go` for the rationale.
12197                if Self::ast_type_is_void(ret) {
12198                    format!("func({})", param_strs.join(", "))
12199                } else {
12200                    format!(
12201                        "func({}) {}",
12202                        param_strs.join(", "),
12203                        self.ast_type_to_go(ret)
12204                    )
12205                }
12206            }
12207            TypeExpr::Optional { inner, .. } => {
12208                let _ = inner;
12209                "__bockOption".to_string()
12210            }
12211            TypeExpr::SelfType { .. } => "/* Self */".into(),
12212        }
12213    }
12214
12215    // ── Helpers ─────────────────────────────────────────────────────────────
12216
12217    fn emit_block_body(&mut self, node: &AIRNode) -> Result<(), CodegenError> {
12218        self.emit_block_body_inner(node, false)
12219    }
12220
12221    /// Emit a `@test` function body (S7), lowering `expect(...)` assertion chains
12222    /// to Go `if <neg> { t.Errorf(...) }` guards and falling back to the normal
12223    /// statement emitter for any other statement. Sets `needs_reflect` when an
12224    /// equality assertion (`reflect.DeepEqual`) is emitted.
12225    fn emit_go_test_body(&mut self, body: &AIRNode) -> Result<(), CodegenError> {
12226        let stmts_and_tail: Vec<&AIRNode> = match &body.kind {
12227            NodeKind::Block { stmts, tail } => stmts.iter().chain(tail.as_deref()).collect(),
12228            _ => vec![body],
12229        };
12230        for stmt in stmts_and_tail {
12231            if let Some((assertion, actual, expected)) = crate::generator::classify_assertion(stmt)
12232            {
12233                let a = self.expr_to_string(actual)?;
12234                use crate::generator::TestAssertion as T;
12235                match assertion {
12236                    T::Equal => {
12237                        // Go `==` follows the constant-conversion rules (an
12238                        // untyped literal `3` compares equal to an `int64`), so it
12239                        // handles ints/floats/strings/bools and comparable structs
12240                        // (the `__bockOption`/`__bockResult` value runtimes) without
12241                        // the type-mismatch `reflect.DeepEqual(int64, int)` pitfall.
12242                        // Slice/map equality (uncommon in `@test`) would need
12243                        // `reflect.DeepEqual`; that is a known follow-up.
12244                        let e = match expected {
12245                            Some(e) => self.expr_to_string(e)?,
12246                            None => "nil".to_string(),
12247                        };
12248                        self.writeln(&format!("if ({a}) != ({e}) {{"));
12249                        self.indent += 1;
12250                        self.writeln(&format!("t.Errorf(\"expected %v, got %v\", {e}, {a})"));
12251                        self.indent -= 1;
12252                        self.writeln("}");
12253                    }
12254                    T::BeTrue => {
12255                        self.writeln(&format!("if !({a}) {{"));
12256                        self.indent += 1;
12257                        self.writeln("t.Errorf(\"expected true, got false\")");
12258                        self.indent -= 1;
12259                        self.writeln("}");
12260                    }
12261                    T::BeFalse => {
12262                        self.writeln(&format!("if {a} {{"));
12263                        self.indent += 1;
12264                        self.writeln("t.Errorf(\"expected false, got true\")");
12265                        self.indent -= 1;
12266                        self.writeln("}");
12267                    }
12268                    T::BeSome | T::BeNone | T::BeOk | T::BeErr => {
12269                        let tag = match assertion {
12270                            T::BeSome => "Some",
12271                            T::BeNone => "None",
12272                            T::BeOk => "Ok",
12273                            _ => "Err",
12274                        };
12275                        self.writeln(&format!("if ({a}).tag != \"{tag}\" {{"));
12276                        self.indent += 1;
12277                        self.writeln(&format!("t.Errorf(\"expected {tag}, got %v\", ({a}).tag)"));
12278                        self.indent -= 1;
12279                        self.writeln("}");
12280                    }
12281                }
12282            } else {
12283                self.emit_node(stmt)?;
12284            }
12285        }
12286        Ok(())
12287    }
12288
12289    fn emit_block_body_return(&mut self, node: &AIRNode) -> Result<(), CodegenError> {
12290        self.emit_block_body_inner(node, true)
12291    }
12292
12293    fn emit_block_body_inner(
12294        &mut self,
12295        node: &AIRNode,
12296        emit_return: bool,
12297    ) -> Result<(), CodegenError> {
12298        // Open a fresh Go block scope for shadowing-`let` tracking. The frame is
12299        // pre-seeded with any pending parameter names (function/method entry), so
12300        // a `let` shadowing a parameter — which is the *same* Go scope, the body
12301        // gets no extra brace — reassigns rather than re-declares. Popped on exit
12302        // regardless of which return path the body takes.
12303        let mut frame = HashSet::new();
12304        if let Some(seed) = self.pending_scope_seed.take() {
12305            frame.extend(seed);
12306        }
12307        self.go_declared_scopes.push(frame);
12308        let result = self.emit_block_body_inner_scoped(node, emit_return);
12309        self.go_declared_scopes.pop();
12310        result
12311    }
12312
12313    /// Whether `name` is already declared in the innermost open Go block scope.
12314    /// A shadowing `let` of such a name must lower to a plain assignment (`=`),
12315    /// not a fresh `:=` / `var` declaration (which Go rejects as "no new
12316    /// variables on left side of :=").
12317    fn go_name_declared_in_block(&self, name: &str) -> bool {
12318        self.go_declared_scopes
12319            .last()
12320            .is_some_and(|frame| frame.contains(name))
12321    }
12322
12323    /// Record `name` as declared in the innermost open Go block scope.
12324    fn go_record_declared(&mut self, name: &str) {
12325        if let Some(frame) = self.go_declared_scopes.last_mut() {
12326            frame.insert(name.to_string());
12327        }
12328    }
12329
12330    fn emit_block_body_inner_scoped(
12331        &mut self,
12332        node: &AIRNode,
12333        emit_return: bool,
12334    ) -> Result<(), CodegenError> {
12335        if let NodeKind::Block { stmts, tail } = &node.kind {
12336            if stmts.is_empty() && tail.is_none() {
12337                self.writeln("// empty");
12338                return Ok(());
12339            }
12340            // Type the declare-only temps this block introduces (value-CF hoist).
12341            self.seed_decl_only_types(stmts);
12342            for (i, s) in stmts.iter().enumerate() {
12343                self.emit_node(s)?;
12344                // Go rejects a `let`-bound local never read (`declared and not
12345                // used`); Bock permits it (a binding kept for its call's side
12346                // effect, e.g. context-audit's `let payment = process(...)`). When
12347                // a simple `let x = …` binding's name is referenced nowhere in the
12348                // rest of this block, emit `_ = x` to satisfy Go without dropping
12349                // the side-effecting initializer. Restricted to a plain `BindPat`
12350                // (not `_`, tuple/record patterns, or a `:=`-from-`if let` form):
12351                // the conservative reference scan over the *remaining* siblings and
12352                // tail never silences a name that is actually used.
12353                if let NodeKind::LetBinding { pattern, .. } = &s.kind {
12354                    if let NodeKind::BindPat { name, .. } = &pattern.kind {
12355                        let go_name = go_value_ident(&name.name);
12356                        if go_name != "_" {
12357                            let used_after = stmts[i + 1..]
12358                                .iter()
12359                                .chain(tail.as_deref())
12360                                .any(|n| collect_used_idents(n).contains(&name.name));
12361                            if !used_after {
12362                                self.writeln(&format!("_ = {go_name}"));
12363                            }
12364                        }
12365                    }
12366                }
12367            }
12368            if let Some(t) = tail {
12369                // A statement tail (`return`/`break`/`continue`/assignment) is
12370                // emitted as a statement, never via `emit_expr` (which would
12371                // fall through to `/* unsupported */` for control-flow nodes).
12372                if crate::generator::node_is_statement(t) {
12373                    self.emit_node(t)?;
12374                    return Ok(());
12375                }
12376                // DQ18: an in-place `List` mutator (`push`/`append`) in *tail*
12377                // position (a single-statement loop body `{ acc.push(x) }`) is a
12378                // `Void` call, so it carries no value to return/emit — lower it to
12379                // Go's slice-growth assignment statement, not the value-less call
12380                // form `emit_expr` would otherwise emit.
12381                if let NodeKind::Call { callee, args, .. } = &t.kind {
12382                    if self.try_emit_list_mutating_stmt(t, callee, args)? {
12383                        return Ok(());
12384                    }
12385                    // DQ30: the `Void` in-place mutators (`insert`/`reverse`/
12386                    // `set`) lower to valueless func-literal calls — valid Go
12387                    // *statements*, but not values, so a tail must emit them as
12388                    // a plain statement, never behind a `return`.
12389                    if let Some((_, m, _)) =
12390                        crate::generator::desugared_list_inplace_mutator(t, callee, args)
12391                    {
12392                        if matches!(m, "insert" | "reverse" | "set") {
12393                            self.write_indent();
12394                            self.emit_expr(t)?;
12395                            self.buf.push('\n');
12396                            return Ok(());
12397                        }
12398                    }
12399                }
12400                // A `match` whose value is not consumed must emit in statement
12401                // position (a Go `switch` / if-chain), never as an expression
12402                // IIFE. Two cases reach here:
12403                //
12404                //   1. A `match` with statement arms (`break`/`return`/an
12405                //      assignment) carries no value at all.
12406                //   2. A `match` in a *non-returning* tail position
12407                //      (`emit_return == false`: a `Void`/`main` body tail, an
12408                //      `if`/loop/arm statement block) — its result is discarded,
12409                //      so even value-shaped arms (a `print(...)`/void-call arm,
12410                //      classified as an expression, not a statement) yield
12411                //      nothing the caller reads.
12412                //
12413                // Routing both to `emit_match` avoids the
12414                // `func() interface{} { …arm…; panic("unreachable") }()` IIFE
12415                // form, whose trailing exhaustiveness guard runs *after* the
12416                // matched arm — producing correct output and THEN a spurious
12417                // `unreachable` panic (Q-go-tailmatch-unreachable-panic). The
12418                // statement-position lowering has no such guard for a total
12419                // record/plain match, so the arm is the terminal statement.
12420                if let NodeKind::Match { scrutinee, arms } = &t.kind {
12421                    if !emit_return || crate::generator::match_has_statement_arm(arms) {
12422                        self.emit_match(scrutinee, arms)?;
12423                        return Ok(());
12424                    }
12425                }
12426                let should_return = emit_return && !self.is_void_call(t);
12427                // A collection literal in *tail-return* position adopts the
12428                // function's return collection element type(s), mirroring the
12429                // explicit-`return` arm, so `fn single[T](x: T) -> List[T] { [x]
12430                // }` emits `[]T{x}` rather than `[]interface{}{x}`.
12431                let prev_expected = self.expected_collection_elem.take();
12432                if should_return
12433                    && matches!(
12434                        t.kind,
12435                        NodeKind::ListLiteral { .. }
12436                            | NodeKind::MapLiteral { .. }
12437                            | NodeKind::SetLiteral { .. }
12438                    )
12439                {
12440                    self.expected_collection_elem = self.current_fn_ret_collection_elem.clone();
12441                }
12442                // A generic-record construction in tail-return position adopts
12443                // the function's rendered return type as its expected type, so
12444                // `fn list_iter[T](xs: List[T]) -> ListIterator[T] {
12445                // ListIterator { xs: xs, cursor: 0 } }` emits `ListIterator[T]{
12446                // ... }` rather than the field-inference `[any]` fallback (Go
12447                // requires explicit type args on a generic struct literal).
12448                let prev_expected_type = self.current_expected_type.take();
12449                if should_return
12450                    && (matches!(
12451                        t.kind,
12452                        NodeKind::RecordConstruct { .. } | NodeKind::TupleLiteral { .. }
12453                    ) || Self::is_expr_optional_or_result_match(t))
12454                {
12455                    self.current_expected_type = self.current_fn_ret_type.clone();
12456                }
12457                // A lambda returned directly (`-> Fn(Int) -> Int { (x) => … }`)
12458                // takes its param/return types from the declared function type.
12459                let saved_lambda_hints = if should_return {
12460                    self.pin_return_lambda_types(t)
12461                } else {
12462                    (
12463                        self.expected_lambda_param_types.clone(),
12464                        self.forced_lambda_ret.clone(),
12465                    )
12466                };
12467                if should_return {
12468                    let ind = self.indent_str();
12469                    let _ = write!(self.buf, "{ind}return ");
12470                    self.emit_expr(t)?;
12471                    self.buf.push('\n');
12472                } else {
12473                    self.write_indent();
12474                    self.emit_expr(t)?;
12475                    self.buf.push('\n');
12476                }
12477                self.expected_lambda_param_types = saved_lambda_hints.0;
12478                self.forced_lambda_ret = saved_lambda_hints.1;
12479                self.expected_collection_elem = prev_expected;
12480                self.current_expected_type = prev_expected_type;
12481            }
12482        } else if crate::generator::node_is_statement(node) {
12483            // A bare statement body (`break`/`continue`/`return`/assignment):
12484            // emit through the statement path, never as an expression.
12485            self.emit_node(node)?;
12486        } else {
12487            // Single expression as body. A `match` whose value is not consumed
12488            // (statement arms, or any match in a non-returning `emit_return ==
12489            // false` body) emits in statement position, never as an IIFE — see
12490            // the block-tail arm above (Q-go-tailmatch-unreachable-panic).
12491            if let NodeKind::Match { scrutinee, arms } = &node.kind {
12492                if !emit_return || crate::generator::match_has_statement_arm(arms) {
12493                    self.emit_match(scrutinee, arms)?;
12494                    return Ok(());
12495                }
12496            }
12497            let should_return = emit_return && !self.is_void_call(node);
12498            let prev_expected = self.expected_collection_elem.take();
12499            if should_return
12500                && matches!(
12501                    node.kind,
12502                    NodeKind::ListLiteral { .. }
12503                        | NodeKind::MapLiteral { .. }
12504                        | NodeKind::SetLiteral { .. }
12505                )
12506            {
12507                self.expected_collection_elem = self.current_fn_ret_collection_elem.clone();
12508            }
12509            // See the block-tail arm: a generic-record construction — or an
12510            // expression-position `Optional`/`Result` match — as the sole body
12511            // expression adopts the function's return type, so its IIFE result
12512            // is assignable to the declared return type.
12513            let prev_expected_type = self.current_expected_type.take();
12514            if should_return
12515                && (matches!(
12516                    node.kind,
12517                    NodeKind::RecordConstruct { .. } | NodeKind::TupleLiteral { .. }
12518                ) || Self::is_expr_optional_or_result_match(node))
12519            {
12520                self.current_expected_type = self.current_fn_ret_type.clone();
12521            }
12522            // A lambda body returned directly (`-> Fn(Int) -> Int { (x) => … }`,
12523            // or the single-expression form `compose(...) { (x) => f(g(x)) }`)
12524            // takes its param/return types from the declared function type.
12525            let saved_lambda_hints = if should_return {
12526                self.pin_return_lambda_types(node)
12527            } else {
12528                (
12529                    self.expected_lambda_param_types.clone(),
12530                    self.forced_lambda_ret.clone(),
12531                )
12532            };
12533            if should_return {
12534                let ind = self.indent_str();
12535                let _ = write!(self.buf, "{ind}return ");
12536                self.emit_expr(node)?;
12537                self.buf.push('\n');
12538            } else {
12539                self.write_indent();
12540                self.emit_expr(node)?;
12541                self.buf.push('\n');
12542            }
12543            self.expected_lambda_param_types = saved_lambda_hints.0;
12544            self.forced_lambda_ret = saved_lambda_hints.1;
12545            self.expected_collection_elem = prev_expected;
12546            self.current_expected_type = prev_expected_type;
12547        }
12548        Ok(())
12549    }
12550
12551    /// Whether a statement-position tail node *unconditionally terminates* its
12552    /// enclosing block — a `return`/`break`/`continue`. Used by the block
12553    /// expression-IIFE to decide whether a trailing fallback
12554    /// (`return nil` / `panic("unreachable")`) is reachable: when the tail
12555    /// terminates, the fallback would be dead code after a `return`. An
12556    /// assignment tail (the other statement-tail shape) does not terminate, so
12557    /// the fallback is still needed there.
12558    fn tail_terminates(node: &AIRNode) -> bool {
12559        matches!(
12560            node.kind,
12561            NodeKind::Return { .. } | NodeKind::Break { .. } | NodeKind::Continue
12562        )
12563    }
12564
12565    /// Emit an `if`/`match` arm body as the value of its enclosing expression
12566    /// IIFE — i.e. as a `return <expr>`, but with a void-call tail handled
12567    /// correctly. A `println(...)` / void effect-op call returns `(int, error)`
12568    /// (Go's `fmt.Println`) or nothing, so `return println(...)` is a Go arity
12569    /// error (`too many return values have (int, error) want T`). When the body's
12570    /// effective tail is a void call, emit the call as a *statement* followed by
12571    /// `return <zero>` (the IIFE result is discarded — these arise only when the
12572    /// whole match/if is in statement position). Otherwise emit the normal
12573    /// `return <body>`. `iife_ty` is the enclosing IIFE's return type; a non-`None`
12574    /// value uses a typed zero, `None` uses `nil`.
12575    fn emit_arm_body_return(
12576        &mut self,
12577        body: &AIRNode,
12578        iife_ty: Option<&str>,
12579    ) -> Result<(), CodegenError> {
12580        // The effective tail is the block tail (when the body is a `{ ... }`
12581        // block) or the body itself (a bare expression arm).
12582        let tail = match &body.kind {
12583            NodeKind::Block { tail: Some(t), .. } => t.as_ref(),
12584            NodeKind::Block { tail: None, .. } => body, // emitted below as-is
12585            _ => body,
12586        };
12587        if self.is_void_call(tail) {
12588            // Emit any leading block statements, then the void call as a
12589            // statement, then a discarded zero return.
12590            if let NodeKind::Block { stmts, .. } = &body.kind {
12591                for s in stmts {
12592                    self.emit_node(s)?;
12593                    self.buf.push_str("; ");
12594                }
12595            }
12596            self.emit_expr(tail)?;
12597            self.buf.push_str("; return ");
12598            match iife_ty {
12599                Some(ty) => self.zero_value_for(ty),
12600                None => self.buf.push_str("nil"),
12601            }
12602            return Ok(());
12603        }
12604        self.buf.push_str("return ");
12605        self.emit_block_as_expr(body)
12606    }
12607
12608    /// Returns `true` if the expression is a call to a known void function
12609    /// (prelude or a Void-returning effect operation).
12610    fn is_void_call(&self, node: &AIRNode) -> bool {
12611        if let NodeKind::Call { callee, .. } = &node.kind {
12612            if let NodeKind::Identifier { name } = &callee.kind {
12613                if matches!(
12614                    name.name.as_str(),
12615                    "println" | "print" | "debug" | "assert" | "todo" | "unreachable"
12616                ) {
12617                    return true;
12618                }
12619                if self.void_effect_ops.contains(&name.name) {
12620                    return true;
12621                }
12622            }
12623        }
12624        false
12625    }
12626
12627    fn emit_block_as_expr(&mut self, node: &AIRNode) -> Result<(), CodegenError> {
12628        if let NodeKind::Block { stmts, tail } = &node.kind {
12629            if stmts.is_empty() {
12630                if let Some(t) = tail {
12631                    return self.emit_expr(t);
12632                }
12633            }
12634        }
12635        self.emit_expr(node)
12636    }
12637
12638    fn pattern_to_binding_name(&self, pat: &AIRNode) -> String {
12639        match &pat.kind {
12640            // A bound value name, keyword-escaped. This is the single Go
12641            // value-binding funnel: params, `let` bindings, and the
12642            // scope-inference map keys all derive from it, so the escape lands
12643            // identically everywhere and never strips back to a bare keyword (the
12644            // outer callers no longer re-run `to_camel_case`, which would drop a
12645            // trailing escape `_`).
12646            NodeKind::BindPat { name, .. } => go_value_ident(&name.name),
12647            NodeKind::WildcardPat => "_".into(),
12648            NodeKind::TuplePat { elems } => {
12649                // Go doesn't have tuple destructuring; use first element.
12650                elems
12651                    .first()
12652                    .map(|e| self.pattern_to_binding_name(e))
12653                    .unwrap_or_else(|| "_".into())
12654            }
12655            NodeKind::RecordPat { fields, .. } => fields
12656                .first()
12657                .map(|f| to_camel_case(&f.name.name))
12658                .unwrap_or_else(|| "_".into()),
12659            _ => "_".into(),
12660        }
12661    }
12662
12663    fn pattern_to_go_binding(&self, pat: &AIRNode) -> String {
12664        self.pattern_to_binding_name(pat)
12665    }
12666
12667    fn type_expr_to_string(&self, node: &AIRNode) -> String {
12668        match &node.kind {
12669            NodeKind::TypeNamed { path, .. } => path
12670                .segments
12671                .iter()
12672                .map(|s| s.name.as_str())
12673                .collect::<Vec<_>>()
12674                .join("."),
12675            NodeKind::Identifier { name } => name.name.clone(),
12676            _ => "Unknown".into(),
12677        }
12678    }
12679}
12680
12681// ─── Utility functions ───────────────────────────────────────────────────────
12682
12683/// Map a Bock `@test` function name to a valid Go test function name
12684/// (`TestXxx`), as `go test` requires (S7).
12685///
12686/// A leading `test`/`test_` is stripped so the conventional `test_add` does not
12687/// become the stuttering `TestTestAdd`; the remainder is PascalCased and
12688/// `Test`-prefixed. A name that is *only* `test` (no suffix) keeps a stable
12689/// disambiguating form (`TestTest` → `Test_` is invalid, so it becomes
12690/// `TestCase`). Empty input falls back to `TestCase`.
12691fn go_test_fn_name(bock_name: &str) -> String {
12692    let stripped = bock_name
12693        .strip_prefix("test_")
12694        .or_else(|| bock_name.strip_prefix("test"))
12695        .unwrap_or(bock_name);
12696    let pascal = to_pascal_case(stripped);
12697    if pascal.is_empty() {
12698        "TestCase".to_string()
12699    } else {
12700        format!("Test{pascal}")
12701    }
12702}
12703
12704/// True for Bock's built-in Optional/Result constructors, which must be
12705/// emitted verbatim (PascalCase preserved) so generated Go code can match
12706/// the runtime prelude's `Some`/`None`/`Ok`/`Err` types.
12707fn is_prelude_ctor(s: &str) -> bool {
12708    matches!(s, "Some" | "None" | "Ok" | "Err")
12709}
12710
12711/// Convert a Bock *value* identifier (a param, local binding, or private
12712/// free-function name) to its Go form: `camelCase`, then escaped against the Go
12713/// keyword set so a binding named e.g. `default`/`range`/`type` emits
12714/// `default_`/`range_`/`type_` rather than the illegal bare keyword. Apply at
12715/// every value declaration and reference site **and** the type-inference
12716/// scope-map keys, so the escaped name is used uniformly and the maps stay
12717/// aligned with the emitted name. Member/field/method names and exported
12718/// (PascalCased) names use bare casing — a keyword is legal as a Go field name,
12719/// and PascalCasing already lifts a name out of the lowercase keyword set.
12720/// See [`crate::generator::escape_target_keyword`].
12721fn go_value_ident(name: &str) -> String {
12722    crate::generator::escape_target_keyword(
12723        &to_camel_case(name),
12724        crate::generator::KeywordTarget::Go,
12725    )
12726}
12727
12728/// Convert a name to `camelCase` (Go unexported).
12729fn to_camel_case(s: &str) -> String {
12730    if s.is_empty() || s == "_" {
12731        return s.to_string();
12732    }
12733    // If already camelCase (starts lowercase, no underscores), return as-is.
12734    if !s.contains('_') && s.starts_with(|c: char| c.is_lowercase()) {
12735        return s.to_string();
12736    }
12737    // If it's snake_case, convert to camelCase.
12738    if s.contains('_') {
12739        let parts: Vec<&str> = s.split('_').filter(|p| !p.is_empty()).collect();
12740        if parts.is_empty() {
12741            return s.to_string();
12742        }
12743        let mut result = parts[0].to_lowercase();
12744        for part in &parts[1..] {
12745            let mut chars = part.chars();
12746            if let Some(first) = chars.next() {
12747                result.push(
12748                    first
12749                        .to_uppercase()
12750                        .next()
12751                        .expect("uppercase yields at least one char"),
12752                );
12753                result.extend(chars);
12754            }
12755        }
12756        return result;
12757    }
12758    // If PascalCase, lowercase first letter.
12759    let mut chars = s.chars();
12760    let first = chars.next().expect("non-empty string guaranteed by caller");
12761    let mut result = first.to_lowercase().to_string();
12762    result.extend(chars);
12763    result
12764}
12765
12766/// Returns true if `name` is the identifier of a Duration or Instant instance
12767/// method. Used to recognise `d.as_millis()` / `i.elapsed()` calls during codegen.
12768fn is_time_method_name(name: &str) -> bool {
12769    matches!(
12770        name,
12771        "as_nanos"
12772            | "as_millis"
12773            | "as_seconds"
12774            | "is_zero"
12775            | "is_negative"
12776            | "abs"
12777            | "elapsed"
12778            | "duration_since"
12779    )
12780}
12781
12782/// Convert a name to `PascalCase` (Go exported).
12783fn to_pascal_case(s: &str) -> String {
12784    if s.is_empty() || s == "_" {
12785        return s.to_string();
12786    }
12787    // If it's snake_case, convert to PascalCase.
12788    if s.contains('_') {
12789        let parts: Vec<&str> = s.split('_').filter(|p| !p.is_empty()).collect();
12790        let mut result = String::new();
12791        for part in &parts {
12792            let mut chars = part.chars();
12793            if let Some(first) = chars.next() {
12794                result.push(
12795                    first
12796                        .to_uppercase()
12797                        .next()
12798                        .expect("uppercase yields at least one char"),
12799                );
12800                result.extend(chars);
12801            }
12802        }
12803        return result;
12804    }
12805    // Already PascalCase or camelCase — uppercase first letter.
12806    let mut chars = s.chars();
12807    let first = chars.next().expect("non-empty string guaranteed by caller");
12808    let mut result = first.to_uppercase().to_string();
12809    result.extend(chars);
12810    result
12811}
12812
12813/// Escape special characters in a Go string literal.
12814fn escape_go_string(s: &str) -> String {
12815    let mut out = String::with_capacity(s.len());
12816    for ch in s.chars() {
12817        match ch {
12818            '"' => out.push_str("\\\""),
12819            '\\' => out.push_str("\\\\"),
12820            '\n' => out.push_str("\\n"),
12821            '\r' => out.push_str("\\r"),
12822            '\t' => out.push_str("\\t"),
12823            _ => out.push(ch),
12824        }
12825    }
12826    out
12827}
12828
12829/// Render a literal as a Go value expression — used by the if-chain match
12830/// lowering to compare a scrutinee against a literal pattern (`<access> == …`).
12831/// Render a `RangePat` bound (`lo`/`hi`) as a Go expression. Range bounds are
12832/// literals (`1..10`) or a const identifier (`MIN..MAX`); anything else falls
12833/// back to the wrapped literal/identifier text, or `0` for an unrecognised node.
12834/// Mirrors `range_bound_to_js`.
12835fn range_bound_to_go(node: &AIRNode) -> String {
12836    match &node.kind {
12837        NodeKind::LiteralPat { lit } => go_literal(lit),
12838        NodeKind::Literal { lit } => go_literal(lit),
12839        NodeKind::Identifier { name } => go_value_ident(&name.name),
12840        _ => "0".to_string(),
12841    }
12842}
12843
12844fn go_literal(lit: &Literal) -> String {
12845    match lit {
12846        Literal::Int(s) | Literal::Float(s) => s.clone(),
12847        Literal::Bool(b) => {
12848            if *b {
12849                "true".to_string()
12850            } else {
12851                "false".to_string()
12852            }
12853        }
12854        Literal::Char(s) => format!("'{s}'"),
12855        Literal::String(s) => format!("\"{}\"", escape_go_string(s)),
12856        Literal::Unit => "nil".to_string(),
12857    }
12858}
12859
12860/// Wrap a raw `interface{}` access (a container's `.v` payload) with the type
12861/// assertion the *child* pattern needs to read it as a typed value. An Optional
12862/// / Result child re-asserts to its runtime struct so `.tag`/`.v` are reachable;
12863/// everything else (bind / wildcard / literal / tuple) reads the raw value.
12864fn go_typed_access(child: &AIRNode, raw_access: &str) -> String {
12865    if let NodeKind::ConstructorPat { path, .. } = &child.kind {
12866        let leaf = path.segments.last().map_or("", |s| s.name.as_str());
12867        match leaf {
12868            "Some" | "None" => return format!("{raw_access}.(__bockOption)"),
12869            "Ok" | "Err" => return format!("{raw_access}.(__bockResult)"),
12870            _ => {}
12871        }
12872    }
12873    raw_access.to_string()
12874}
12875
12876// ─── Tests ───────────────────────────────────────────────────────────────────
12877
12878#[cfg(test)]
12879mod tests {
12880    use super::*;
12881    use bock_air::{AirArg, AirMapEntry, AirRecordField};
12882    use bock_ast::{Ident, TypePath};
12883    use bock_errors::{FileId, Span};
12884
12885    fn span() -> Span {
12886        Span {
12887            file: FileId(0),
12888            start: 0,
12889            end: 0,
12890        }
12891    }
12892
12893    fn ident(name: &str) -> Ident {
12894        Ident {
12895            name: name.to_string(),
12896            span: span(),
12897        }
12898    }
12899
12900    fn type_path(segments: &[&str]) -> TypePath {
12901        TypePath {
12902            segments: segments.iter().map(|s| ident(s)).collect(),
12903            span: span(),
12904        }
12905    }
12906
12907    fn node(id: u32, kind: NodeKind) -> AIRNode {
12908        AIRNode::new(id, span(), kind)
12909    }
12910
12911    fn int_lit(id: u32, val: &str) -> AIRNode {
12912        node(
12913            id,
12914            NodeKind::Literal {
12915                lit: Literal::Int(val.into()),
12916            },
12917        )
12918    }
12919
12920    fn str_lit(id: u32, val: &str) -> AIRNode {
12921        node(
12922            id,
12923            NodeKind::Literal {
12924                lit: Literal::String(val.into()),
12925            },
12926        )
12927    }
12928
12929    fn bool_lit(id: u32, val: bool) -> AIRNode {
12930        node(
12931            id,
12932            NodeKind::Literal {
12933                lit: Literal::Bool(val),
12934            },
12935        )
12936    }
12937
12938    fn id_node(id: u32, name: &str) -> AIRNode {
12939        node(id, NodeKind::Identifier { name: ident(name) })
12940    }
12941
12942    fn bind_pat(id: u32, name: &str) -> AIRNode {
12943        node(
12944            id,
12945            NodeKind::BindPat {
12946                name: ident(name),
12947                is_mut: false,
12948            },
12949        )
12950    }
12951
12952    fn param_node(id: u32, name: &str) -> AIRNode {
12953        node(
12954            id,
12955            NodeKind::Param {
12956                pattern: Box::new(bind_pat(id + 100, name)),
12957                ty: None,
12958                default: None,
12959            },
12960        )
12961    }
12962
12963    fn typed_param_node(id: u32, name: &str, ty_name: &str) -> AIRNode {
12964        node(
12965            id,
12966            NodeKind::Param {
12967                pattern: Box::new(bind_pat(id + 100, name)),
12968                ty: Some(Box::new(node(
12969                    id + 200,
12970                    NodeKind::TypeNamed {
12971                        path: type_path(&[ty_name]),
12972                        args: vec![],
12973                    },
12974                ))),
12975                default: None,
12976            },
12977        )
12978    }
12979
12980    fn block(id: u32, stmts: Vec<AIRNode>, tail: Option<AIRNode>) -> AIRNode {
12981        node(
12982            id,
12983            NodeKind::Block {
12984                stmts,
12985                tail: tail.map(Box::new),
12986            },
12987        )
12988    }
12989
12990    fn module(imports: Vec<AIRNode>, items: Vec<AIRNode>) -> AIRNode {
12991        node(
12992            0,
12993            NodeKind::Module {
12994                path: None,
12995                annotations: vec![],
12996                imports,
12997                items,
12998            },
12999        )
13000    }
13001
13002    fn gen(module: &AIRNode) -> String {
13003        let gen = GoGenerator::new();
13004        let result = gen.generate_module(module).unwrap();
13005        result.files[0].content.clone()
13006    }
13007
13008    // ── Basic tests ─────────────────────────────────────────────────────────
13009
13010    #[test]
13011    fn implements_code_generator_trait() {
13012        let gen = GoGenerator::new();
13013        assert_eq!(gen.target().id, "go");
13014    }
13015
13016    #[test]
13017    fn empty_module() {
13018        let m = module(vec![], vec![]);
13019        let out = gen(&m);
13020        assert!(out.contains("package main"), "got: {out}");
13021    }
13022
13023    #[test]
13024    fn simple_function() {
13025        let body = block(2, vec![], Some(int_lit(3, "42")));
13026        let f = node(
13027            1,
13028            NodeKind::FnDecl {
13029                annotations: vec![],
13030                visibility: Visibility::Private,
13031                is_async: false,
13032                name: ident("answer"),
13033                generic_params: vec![],
13034                params: vec![],
13035                return_type: None,
13036                effect_clause: vec![],
13037                where_clause: vec![],
13038                body: Box::new(body),
13039            },
13040        );
13041        let out = gen(&module(vec![], vec![f]));
13042        assert!(out.contains("func answer()"), "got: {out}");
13043        assert!(out.contains("return 42"), "got: {out}");
13044    }
13045
13046    #[test]
13047    fn public_function_is_pascal_case() {
13048        let body = block(2, vec![], Some(int_lit(3, "42")));
13049        let f = node(
13050            1,
13051            NodeKind::FnDecl {
13052                annotations: vec![],
13053                visibility: Visibility::Public,
13054                is_async: false,
13055                name: ident("getAnswer"),
13056                generic_params: vec![],
13057                params: vec![],
13058                return_type: None,
13059                effect_clause: vec![],
13060                where_clause: vec![],
13061                body: Box::new(body),
13062            },
13063        );
13064        let out = gen(&module(vec![], vec![f]));
13065        assert!(out.contains("func GetAnswer()"), "got: {out}");
13066    }
13067
13068    #[test]
13069    fn function_with_params_and_types() {
13070        let body = block(
13071            5,
13072            vec![],
13073            Some(node(
13074                6,
13075                NodeKind::BinaryOp {
13076                    op: BinOp::Add,
13077                    left: Box::new(id_node(7, "a")),
13078                    right: Box::new(id_node(8, "b")),
13079                },
13080            )),
13081        );
13082        let f = node(
13083            1,
13084            NodeKind::FnDecl {
13085                annotations: vec![],
13086                visibility: Visibility::Public,
13087                is_async: false,
13088                name: ident("add"),
13089                generic_params: vec![],
13090                params: vec![
13091                    typed_param_node(2, "a", "Int"),
13092                    typed_param_node(3, "b", "Int"),
13093                ],
13094                return_type: Some(Box::new(node(
13095                    4,
13096                    NodeKind::TypeNamed {
13097                        path: type_path(&["Int"]),
13098                        args: vec![],
13099                    },
13100                ))),
13101                effect_clause: vec![],
13102                where_clause: vec![],
13103                body: Box::new(body),
13104            },
13105        );
13106        let out = gen(&module(vec![], vec![f]));
13107        assert!(
13108            out.contains("func Add(a int64, b int64) int64 {"),
13109            "got: {out}"
13110        );
13111        assert!(out.contains("(a + b)"), "got: {out}");
13112    }
13113
13114    #[test]
13115    fn record_to_struct() {
13116        let rec = node(
13117            1,
13118            NodeKind::RecordDecl {
13119                annotations: vec![],
13120                visibility: Visibility::Public,
13121                name: ident("Point"),
13122                generic_params: vec![],
13123                fields: vec![
13124                    bock_ast::RecordDeclField {
13125                        id: 0,
13126                        span: span(),
13127                        name: ident("x"),
13128                        ty: TypeExpr::Named {
13129                            id: 0,
13130                            span: span(),
13131                            path: type_path(&["Float"]),
13132                            args: vec![],
13133                        },
13134                        default: None,
13135                    },
13136                    bock_ast::RecordDeclField {
13137                        id: 1,
13138                        span: span(),
13139                        name: ident("y"),
13140                        ty: TypeExpr::Named {
13141                            id: 1,
13142                            span: span(),
13143                            path: type_path(&["Float"]),
13144                            args: vec![],
13145                        },
13146                        default: None,
13147                    },
13148                ],
13149            },
13150        );
13151        let out = gen(&module(vec![], vec![rec]));
13152        assert!(out.contains("type Point struct {"), "got: {out}");
13153        assert!(out.contains("X\tfloat64"), "got: {out}");
13154        assert!(out.contains("Y\tfloat64"), "got: {out}");
13155    }
13156
13157    #[test]
13158    fn trait_to_interface() {
13159        let t = node(
13160            1,
13161            NodeKind::TraitDecl {
13162                annotations: vec![],
13163                visibility: Visibility::Public,
13164                is_platform: false,
13165                name: ident("Drawable"),
13166                generic_params: vec![],
13167                associated_types: vec![],
13168                methods: vec![node(
13169                    2,
13170                    NodeKind::FnDecl {
13171                        annotations: vec![],
13172                        visibility: Visibility::Public,
13173                        is_async: false,
13174                        name: ident("draw"),
13175                        generic_params: vec![],
13176                        params: vec![],
13177                        return_type: None,
13178                        effect_clause: vec![],
13179                        where_clause: vec![],
13180                        body: Box::new(block(3, vec![], None)),
13181                    },
13182                )],
13183            },
13184        );
13185        let out = gen(&module(vec![], vec![t]));
13186        assert!(out.contains("type Drawable interface {"), "got: {out}");
13187        assert!(out.contains("Draw()"), "got: {out}");
13188    }
13189
13190    #[test]
13191    fn self_operand_trait_becomes_f_bounded_generic_interface() {
13192        // P2 item 4: a trait whose method takes a `Self` operand
13193        // (`compare(self, other: Self)`) is encoded as an F-bounded generic
13194        // interface so an impl `func (Key) Compare(Key)` can satisfy it and a
13195        // bound `[T: Comparable]` lowers to `[T Comparable[T]]`. The leading
13196        // `self` receiver is dropped (implicit in a Go interface method); `Self`
13197        // renders as the interface's `__Self` type param.
13198        let self_param = node(
13199            10,
13200            NodeKind::Param {
13201                pattern: Box::new(bind_pat(11, "self")),
13202                ty: None,
13203                default: None,
13204            },
13205        );
13206        let other_param = node(
13207            12,
13208            NodeKind::Param {
13209                pattern: Box::new(bind_pat(13, "other")),
13210                ty: Some(Box::new(node(14, NodeKind::TypeSelf))),
13211                default: None,
13212            },
13213        );
13214        let method = node(
13215            2,
13216            NodeKind::FnDecl {
13217                annotations: vec![],
13218                visibility: Visibility::Public,
13219                is_async: false,
13220                name: ident("compare"),
13221                generic_params: vec![],
13222                params: vec![self_param, other_param],
13223                return_type: Some(Box::new(node(
13224                    20,
13225                    NodeKind::TypeNamed {
13226                        path: type_path(&["Bool"]),
13227                        args: vec![],
13228                    },
13229                ))),
13230                effect_clause: vec![],
13231                where_clause: vec![],
13232                body: Box::new(block(3, vec![], None)),
13233            },
13234        );
13235        let t = node(
13236            1,
13237            NodeKind::TraitDecl {
13238                annotations: vec![],
13239                visibility: Visibility::Public,
13240                is_platform: false,
13241                name: ident("Comparable"),
13242                generic_params: vec![],
13243                associated_types: vec![],
13244                methods: vec![method],
13245            },
13246        );
13247        let out = gen(&module(vec![], vec![t]));
13248        assert!(
13249            out.contains("type Comparable[__Self any] interface {"),
13250            "self-operand trait should be an F-bounded generic interface, got: {out}"
13251        );
13252        assert!(
13253            out.contains("Compare(__Self)"),
13254            "the `self` receiver is dropped and `Self` renders as `__Self`, got: {out}"
13255        );
13256    }
13257
13258    #[test]
13259    fn enum_to_interface_and_structs() {
13260        let e = node(
13261            1,
13262            NodeKind::EnumDecl {
13263                annotations: vec![],
13264                visibility: Visibility::Public,
13265                name: ident("Shape"),
13266                generic_params: vec![],
13267                variants: vec![
13268                    node(
13269                        2,
13270                        NodeKind::EnumVariant {
13271                            name: ident("Circle"),
13272                            payload: EnumVariantPayload::Struct(vec![bock_ast::RecordDeclField {
13273                                id: 0,
13274                                span: span(),
13275                                name: ident("radius"),
13276                                ty: TypeExpr::Named {
13277                                    id: 0,
13278                                    span: span(),
13279                                    path: type_path(&["Float"]),
13280                                    args: vec![],
13281                                },
13282                                default: None,
13283                            }]),
13284                        },
13285                    ),
13286                    node(
13287                        3,
13288                        NodeKind::EnumVariant {
13289                            name: ident("None"),
13290                            payload: EnumVariantPayload::Unit,
13291                        },
13292                    ),
13293                ],
13294            },
13295        );
13296        let out = gen(&module(vec![], vec![e]));
13297        assert!(out.contains("type Shape interface {"), "got: {out}");
13298        assert!(out.contains("isShape()"), "got: {out}");
13299        assert!(out.contains("type ShapeCircle struct {"), "got: {out}");
13300        assert!(out.contains("Radius\tfloat64"), "got: {out}");
13301        assert!(out.contains("type ShapeNone struct{}"), "got: {out}");
13302        assert!(
13303            out.contains("func (ShapeCircle) isShape() {}"),
13304            "got: {out}"
13305        );
13306        assert!(out.contains("func (ShapeNone) isShape() {}"), "got: {out}");
13307    }
13308
13309    /// Q-go-generic-enum-codegen: a *generic* user enum (`enum Box[T] { Full(T),
13310    /// Empty }`) must emit Go that actually compiles. The per-variant structs and
13311    /// the sealed interface all carry the enum's `[T any]` params, so:
13312    ///   - the marker-method *receiver* uses the bare `[T]` form
13313    ///     (`func (BoxFull[T]) isBox()`) — the constrained `[T any]` receiver is a
13314    ///     Go syntax error ("unexpected name any, expected ]");
13315    ///   - a construction spells the concrete instantiation (`BoxFull[int64]{…}`,
13316    ///     `BoxEmpty[int64]{}`) recovered from the binding's expected type — Go
13317    ///     rejects a bare generic struct literal;
13318    ///   - a type-switch `case` spells it too (`case BoxFull[int64]:`).
13319    #[test]
13320    fn generic_user_enum_emits_valid_go() {
13321        // enum Box[T] { Full(T), Empty }
13322        let e = node(
13323            1,
13324            NodeKind::EnumDecl {
13325                annotations: vec![],
13326                visibility: Visibility::Public,
13327                name: ident("Box"),
13328                generic_params: vec![generic_param(2, "T")],
13329                variants: vec![
13330                    node(
13331                        3,
13332                        NodeKind::EnumVariant {
13333                            name: ident("Full"),
13334                            payload: EnumVariantPayload::Tuple(vec![named_type(4, "T")]),
13335                        },
13336                    ),
13337                    node(
13338                        5,
13339                        NodeKind::EnumVariant {
13340                            name: ident("Empty"),
13341                            payload: EnumVariantPayload::Unit,
13342                        },
13343                    ),
13344                ],
13345            },
13346        );
13347        // `b: Box[Int]` parameter (a generic instantiation, args = [Int]).
13348        let box_int = |id: u32| {
13349            node(
13350                id,
13351                NodeKind::TypeNamed {
13352                    path: type_path(&["Box"]),
13353                    args: vec![named_type(id + 1, "Int")],
13354                },
13355            )
13356        };
13357        let b_param = node(
13358            20,
13359            NodeKind::Param {
13360                pattern: Box::new(bind_pat(21, "b")),
13361                ty: Some(Box::new(box_int(22))),
13362                default: None,
13363            },
13364        );
13365        // match b { Full(x) => return x  Empty => return 0 }
13366        let full_arm = node(
13367            30,
13368            NodeKind::MatchArm {
13369                pattern: Box::new(node(
13370                    31,
13371                    NodeKind::ConstructorPat {
13372                        path: type_path(&["Full"]),
13373                        fields: vec![bind_pat(32, "x")],
13374                    },
13375                )),
13376                guard: None,
13377                body: Box::new(block(
13378                    33,
13379                    vec![],
13380                    Some(node(
13381                        34,
13382                        NodeKind::Return {
13383                            value: Some(Box::new(id_node(35, "x"))),
13384                        },
13385                    )),
13386                )),
13387            },
13388        );
13389        let empty_arm = node(
13390            36,
13391            NodeKind::MatchArm {
13392                pattern: Box::new(node(
13393                    37,
13394                    NodeKind::ConstructorPat {
13395                        path: type_path(&["Empty"]),
13396                        fields: vec![],
13397                    },
13398                )),
13399                guard: None,
13400                body: Box::new(block(
13401                    38,
13402                    vec![],
13403                    Some(node(
13404                        39,
13405                        NodeKind::Return {
13406                            value: Some(Box::new(int_lit(40, "0"))),
13407                        },
13408                    )),
13409                )),
13410            },
13411        );
13412        let match_expr = node(
13413            41,
13414            NodeKind::Match {
13415                scrutinee: Box::new(id_node(42, "b")),
13416                arms: vec![full_arm, empty_arm],
13417            },
13418        );
13419        let unwrap_fn = node(
13420            50,
13421            NodeKind::FnDecl {
13422                annotations: vec![],
13423                visibility: Visibility::Private,
13424                is_async: false,
13425                name: ident("unwrap_or_zero"),
13426                generic_params: vec![],
13427                params: vec![b_param],
13428                return_type: Some(Box::new(named_type(51, "Int"))),
13429                effect_clause: vec![],
13430                where_clause: vec![],
13431                body: Box::new(block(52, vec![match_expr], None)),
13432            },
13433        );
13434        // fn build() -> Void { let f: Box[Int] = Full(7); let e: Box[Int] = Empty }
13435        let let_f = node(
13436            60,
13437            NodeKind::LetBinding {
13438                is_mut: false,
13439                pattern: Box::new(bind_pat(61, "f")),
13440                ty: Some(Box::new(box_int(62))),
13441                value: Box::new(node(
13442                    63,
13443                    NodeKind::Call {
13444                        callee: Box::new(id_node(64, "Full")),
13445                        type_args: vec![],
13446                        args: vec![AirArg {
13447                            label: None,
13448                            value: int_lit(65, "7"),
13449                        }],
13450                    },
13451                )),
13452            },
13453        );
13454        let let_e = node(
13455            66,
13456            NodeKind::LetBinding {
13457                is_mut: false,
13458                pattern: Box::new(bind_pat(67, "e")),
13459                ty: Some(Box::new(box_int(68))),
13460                value: Box::new(id_node(69, "Empty")),
13461            },
13462        );
13463        let build_fn = node(
13464            70,
13465            NodeKind::FnDecl {
13466                annotations: vec![],
13467                visibility: Visibility::Private,
13468                is_async: false,
13469                name: ident("build"),
13470                generic_params: vec![],
13471                params: vec![],
13472                return_type: None,
13473                effect_clause: vec![],
13474                where_clause: vec![],
13475                body: Box::new(block(71, vec![let_f, let_e], None)),
13476            },
13477        );
13478        let out = gen(&module(vec![], vec![e, unwrap_fn, build_fn]));
13479        // Marker-method receivers use the bare `[T]` form, never `[T any]`.
13480        assert!(
13481            out.contains("func (BoxFull[T]) isBox() {}"),
13482            "marker receiver must be bare `[T]`, got: {out}"
13483        );
13484        assert!(
13485            out.contains("func (BoxEmpty[T]) isBox() {}"),
13486            "marker receiver must be bare `[T]`, got: {out}"
13487        );
13488        assert!(
13489            !out.contains("[T any])"),
13490            "no receiver may carry the `[T any]` constraint, got: {out}"
13491        );
13492        // The type DECLARATIONS keep the constrained `[T any]` form.
13493        assert!(
13494            out.contains("type BoxFull[T any] struct {"),
13495            "variant struct decl keeps `[T any]`, got: {out}"
13496        );
13497        // Type-switch cases spell the concrete instantiation.
13498        assert!(
13499            out.contains("case BoxFull[int64]:"),
13500            "type-switch case must instantiate, got: {out}"
13501        );
13502        assert!(
13503            out.contains("case BoxEmpty[int64]:"),
13504            "type-switch case must instantiate, got: {out}"
13505        );
13506        // Constructions spell the concrete instantiation.
13507        assert!(
13508            out.contains("BoxFull[int64]{Field0: 7}"),
13509            "tuple-variant construction must instantiate, got: {out}"
13510        );
13511        assert!(
13512            out.contains("BoxEmpty[int64]{}"),
13513            "unit-variant construction must instantiate, got: {out}"
13514        );
13515    }
13516
13517    /// Q-go-enum-return-boxing: a value-position `if` whose branches yield enum
13518    /// variants, returned where the declared type is the sealed enum interface,
13519    /// must lower to a `func() Shape { ... }()` closure (not `func() interface{}`),
13520    /// so the boxed variant struct is assignable to the `Shape` return.
13521    #[test]
13522    fn enum_variant_if_branches_box_into_sealed_interface() {
13523        // enum Shape { Circle, Square }  (both unit for brevity)
13524        let e = node(
13525            1,
13526            NodeKind::EnumDecl {
13527                annotations: vec![],
13528                visibility: Visibility::Public,
13529                name: ident("Shape"),
13530                generic_params: vec![],
13531                variants: vec![
13532                    node(
13533                        2,
13534                        NodeKind::EnumVariant {
13535                            name: ident("Circle"),
13536                            payload: EnumVariantPayload::Unit,
13537                        },
13538                    ),
13539                    node(
13540                        3,
13541                        NodeKind::EnumVariant {
13542                            name: ident("Square"),
13543                            payload: EnumVariantPayload::Unit,
13544                        },
13545                    ),
13546                ],
13547            },
13548        );
13549        // fn pick(big: Bool) -> Shape { if (big) { Circle } else { Square } }
13550        let if_expr = node(
13551            10,
13552            NodeKind::If {
13553                let_pattern: None,
13554                condition: Box::new(id_node(11, "big")),
13555                then_block: Box::new(block(12, vec![], Some(id_node(13, "Circle")))),
13556                else_block: Some(Box::new(block(14, vec![], Some(id_node(15, "Square"))))),
13557            },
13558        );
13559        let f = node(
13560            20,
13561            NodeKind::FnDecl {
13562                annotations: vec![],
13563                visibility: Visibility::Public,
13564                is_async: false,
13565                name: ident("pick"),
13566                generic_params: vec![],
13567                params: vec![typed_param_node(21, "big", "Bool")],
13568                return_type: Some(Box::new(node(
13569                    22,
13570                    NodeKind::TypeNamed {
13571                        path: type_path(&["Shape"]),
13572                        args: vec![],
13573                    },
13574                ))),
13575                effect_clause: vec![],
13576                where_clause: vec![],
13577                body: Box::new(block(23, vec![], Some(if_expr))),
13578            },
13579        );
13580        let out = gen(&module(vec![], vec![e, f]));
13581        assert!(
13582            out.contains("func() Shape {"),
13583            "the value-position if must be a `func() Shape` closure (boxed into \
13584             the sealed interface), not a bare-interface closure, got: {out}"
13585        );
13586        assert!(
13587            !out.contains("func() interface{} { if "),
13588            "the if-closure must not fall back to a bare interface, got: {out}"
13589        );
13590        assert!(
13591            out.contains("return ShapeCircle{}") && out.contains("return ShapeSquare{}"),
13592            "each branch returns its variant struct, got: {out}"
13593        );
13594    }
13595
13596    /// Q-go-enum-return-boxing: an UNTYPED `let m = if (..) { Circle } else
13597    /// { Square }` infers the variant's owning enum, so the value closure is
13598    /// typed `func() Shape` rather than the enclosing fn's unrelated return type.
13599    #[test]
13600    fn untyped_let_if_over_variants_infers_enum_iife_type() {
13601        let e = node(
13602            1,
13603            NodeKind::EnumDecl {
13604                annotations: vec![],
13605                visibility: Visibility::Public,
13606                name: ident("Shape"),
13607                generic_params: vec![],
13608                variants: vec![
13609                    node(
13610                        2,
13611                        NodeKind::EnumVariant {
13612                            name: ident("Circle"),
13613                            payload: EnumVariantPayload::Unit,
13614                        },
13615                    ),
13616                    node(
13617                        3,
13618                        NodeKind::EnumVariant {
13619                            name: ident("Square"),
13620                            payload: EnumVariantPayload::Unit,
13621                        },
13622                    ),
13623                ],
13624            },
13625        );
13626        // fn run() -> Void { let m = if (true) { Circle } else { Square } }
13627        let let_binding = node(
13628            10,
13629            NodeKind::LetBinding {
13630                is_mut: false,
13631                pattern: Box::new(bind_pat(11, "m")),
13632                ty: None,
13633                value: Box::new(node(
13634                    12,
13635                    NodeKind::If {
13636                        let_pattern: None,
13637                        condition: Box::new(bool_lit(13, true)),
13638                        then_block: Box::new(block(14, vec![], Some(id_node(15, "Circle")))),
13639                        else_block: Some(Box::new(block(16, vec![], Some(id_node(17, "Square"))))),
13640                    },
13641                )),
13642            },
13643        );
13644        let f = node(
13645            20,
13646            NodeKind::FnDecl {
13647                annotations: vec![],
13648                visibility: Visibility::Public,
13649                is_async: false,
13650                name: ident("run"),
13651                generic_params: vec![],
13652                params: vec![],
13653                return_type: None,
13654                effect_clause: vec![],
13655                where_clause: vec![],
13656                body: Box::new(block(23, vec![let_binding], None)),
13657            },
13658        );
13659        let out = gen(&module(vec![], vec![e, f]));
13660        assert!(
13661            out.contains("m := func() Shape {"),
13662            "an untyped let over variant branches infers the `Shape` closure \
13663             type, got: {out}"
13664        );
13665    }
13666
13667    #[test]
13668    fn effects_as_interface_params() {
13669        let body = block(
13670            3,
13671            vec![node(
13672                4,
13673                NodeKind::LetBinding {
13674                    is_mut: false,
13675                    pattern: Box::new(bind_pat(5, "msg")),
13676                    ty: None,
13677                    value: Box::new(str_lit(6, "hello")),
13678                },
13679            )],
13680            Some(node(
13681                7,
13682                NodeKind::EffectOp {
13683                    effect: type_path(&["Log"]),
13684                    operation: ident("info"),
13685                    args: vec![AirArg {
13686                        label: None,
13687                        value: id_node(8, "msg"),
13688                    }],
13689                },
13690            )),
13691        );
13692        let f = node(
13693            1,
13694            NodeKind::FnDecl {
13695                annotations: vec![],
13696                visibility: Visibility::Public,
13697                is_async: false,
13698                name: ident("process"),
13699                generic_params: vec![],
13700                params: vec![param_node(2, "data")],
13701                return_type: None,
13702                effect_clause: vec![type_path(&["Log"]), type_path(&["Clock"])],
13703                where_clause: vec![],
13704                body: Box::new(body),
13705            },
13706        );
13707        let out = gen(&module(vec![], vec![f]));
13708        assert!(
13709            out.contains("func Process(data interface{}, log Log, clock Clock)"),
13710            "got: {out}"
13711        );
13712        assert!(out.contains("log.Info(msg)"), "got: {out}");
13713    }
13714
13715    /// Q-clock-handler-routing: inside a `with Clock` function the §18.3.1 time
13716    /// builtins route through the in-scope `clock` handler — `Instant.now()` →
13717    /// `clock.NowMonotonic()`, `sleep(d)` → `clock.Sleep(d)`, and the derived
13718    /// `start.elapsed()` via `clock.NowMonotonic().Sub(start)` — NOT the inlined
13719    /// host primitives (`time.Now()` / `time.Sleep`).
13720    #[test]
13721    fn clock_time_ops_route_through_handler() {
13722        let out = gen(&module(vec![], vec![clock_timed_fn()]));
13723        assert!(out.contains("clock.NowMonotonic()"), "got: {out}");
13724        assert!(out.contains("clock.Sleep("), "got: {out}");
13725        assert!(
13726            !out.contains("time.Now()"),
13727            "host clock primitive leaked past the handler: {out}"
13728        );
13729        assert!(
13730            !out.contains("time.Sleep"),
13731            "host sleep primitive leaked past the handler: {out}"
13732        );
13733    }
13734
13735    /// `Duration` / `Instant` used as type annotations must render their Go
13736    /// value representations (`int64` / `time.Time`), not the undefined
13737    /// identifiers, so a `Clock` handler impl compiles (Q-clock-handler-routing
13738    /// supporting fix).
13739    #[test]
13740    fn builtin_time_types_map_to_go() {
13741        let f = node(
13742            1,
13743            NodeKind::FnDecl {
13744                annotations: vec![],
13745                visibility: Visibility::Public,
13746                is_async: false,
13747                name: ident("span"),
13748                generic_params: vec![],
13749                params: vec![typed_param_node(2, "d", "Duration")],
13750                return_type: Some(Box::new(node(
13751                    3,
13752                    NodeKind::TypeNamed {
13753                        path: type_path(&["Instant"]),
13754                        args: vec![],
13755                    },
13756                ))),
13757                effect_clause: vec![],
13758                where_clause: vec![],
13759                body: Box::new(block(10, vec![], None)),
13760            },
13761        );
13762        let out = gen(&module(vec![], vec![f]));
13763        assert!(out.contains("d int64"), "Duration annotation: {out}");
13764        assert!(out.contains("time.Time"), "Instant annotation: {out}");
13765    }
13766
13767    /// Builds `fn timed() with Clock { let start = Instant.now(); sleep(
13768    /// Duration.millis(1)); let d = start.elapsed() }` — the `with Clock` clause
13769    /// puts the `clock` handler in scope so the time builtins route through it.
13770    fn clock_timed_fn() -> AIRNode {
13771        let instant_now = node(
13772            40,
13773            NodeKind::Call {
13774                callee: Box::new(node(
13775                    41,
13776                    NodeKind::FieldAccess {
13777                        object: Box::new(id_node(42, "Instant")),
13778                        field: ident("now"),
13779                    },
13780                )),
13781                args: vec![],
13782                type_args: vec![],
13783            },
13784        );
13785        let duration_millis = node(
13786            50,
13787            NodeKind::Call {
13788                callee: Box::new(node(
13789                    51,
13790                    NodeKind::FieldAccess {
13791                        object: Box::new(id_node(52, "Duration")),
13792                        field: ident("millis"),
13793                    },
13794                )),
13795                args: vec![AirArg {
13796                    label: None,
13797                    value: int_lit(53, "1"),
13798                }],
13799                type_args: vec![],
13800            },
13801        );
13802        let sleep_call = node(
13803            60,
13804            NodeKind::Call {
13805                callee: Box::new(id_node(61, "sleep")),
13806                args: vec![AirArg {
13807                    label: None,
13808                    value: duration_millis,
13809                }],
13810                type_args: vec![],
13811            },
13812        );
13813        let elapsed_call = node(
13814            70,
13815            NodeKind::MethodCall {
13816                receiver: Box::new(id_node(71, "start")),
13817                method: ident("elapsed"),
13818                type_args: vec![],
13819                args: vec![],
13820            },
13821        );
13822        let body = block(
13823            30,
13824            vec![
13825                node(
13826                    31,
13827                    NodeKind::LetBinding {
13828                        is_mut: false,
13829                        pattern: Box::new(bind_pat(32, "start")),
13830                        ty: None,
13831                        value: Box::new(instant_now),
13832                    },
13833                ),
13834                sleep_call,
13835                node(
13836                    33,
13837                    NodeKind::LetBinding {
13838                        is_mut: false,
13839                        pattern: Box::new(bind_pat(34, "d")),
13840                        ty: None,
13841                        value: Box::new(elapsed_call),
13842                    },
13843                ),
13844            ],
13845            None,
13846        );
13847        node(
13848            1,
13849            NodeKind::FnDecl {
13850                annotations: vec![],
13851                visibility: Visibility::Private,
13852                is_async: false,
13853                name: ident("timed"),
13854                generic_params: vec![],
13855                params: vec![],
13856                return_type: None,
13857                effect_clause: vec![type_path(&["Clock"])],
13858                where_clause: vec![],
13859                body: Box::new(body),
13860            },
13861        )
13862    }
13863
13864    #[test]
13865    fn generics_with_type_params() {
13866        let body = block(2, vec![], Some(id_node(3, "value")));
13867        let f = node(
13868            1,
13869            NodeKind::FnDecl {
13870                annotations: vec![],
13871                visibility: Visibility::Public,
13872                is_async: false,
13873                name: ident("identity"),
13874                generic_params: vec![bock_ast::GenericParam {
13875                    id: 10,
13876                    span: span(),
13877                    name: ident("T"),
13878                    bounds: vec![],
13879                }],
13880                params: vec![typed_param_node(2, "value", "T")],
13881                return_type: Some(Box::new(node(
13882                    4,
13883                    NodeKind::TypeNamed {
13884                        path: type_path(&["T"]),
13885                        args: vec![],
13886                    },
13887                ))),
13888                effect_clause: vec![],
13889                where_clause: vec![],
13890                body: Box::new(body),
13891            },
13892        );
13893        let out = gen(&module(vec![], vec![f]));
13894        assert!(
13895            out.contains("func Identity[T any](value T) T {"),
13896            "got: {out}"
13897        );
13898    }
13899
13900    #[test]
13901    fn generics_with_bounds() {
13902        let body = block(2, vec![], Some(id_node(3, "value")));
13903        let f = node(
13904            1,
13905            NodeKind::FnDecl {
13906                annotations: vec![],
13907                visibility: Visibility::Public,
13908                is_async: false,
13909                name: ident("constrained"),
13910                generic_params: vec![bock_ast::GenericParam {
13911                    id: 10,
13912                    span: span(),
13913                    name: ident("T"),
13914                    bounds: vec![type_path(&["Comparable"])],
13915                }],
13916                params: vec![typed_param_node(2, "value", "T")],
13917                return_type: Some(Box::new(node(
13918                    4,
13919                    NodeKind::TypeNamed {
13920                        path: type_path(&["T"]),
13921                        args: vec![],
13922                    },
13923                ))),
13924                effect_clause: vec![],
13925                where_clause: vec![],
13926                body: Box::new(body),
13927            },
13928        );
13929        let out = gen(&module(vec![], vec![f]));
13930        // GAP-C: `Comparable` is a sealed-core trait with no user `impl` in this
13931        // module, so the bound lowers to Go's self-contained ordered constraint
13932        // `__bockOrdered` (there is no `Comparable` type in Go). A user-declared
13933        // `Comparable` trait would keep its name (see `use_core_compare` exec).
13934        assert!(
13935            out.contains("func Constrained[T __bockOrdered](value T) T {"),
13936            "got: {out}"
13937        );
13938    }
13939
13940    #[test]
13941    fn match_to_switch() {
13942        let m = node(
13943            1,
13944            NodeKind::Match {
13945                scrutinee: Box::new(id_node(2, "x")),
13946                arms: vec![
13947                    node(
13948                        3,
13949                        NodeKind::MatchArm {
13950                            pattern: Box::new(node(
13951                                4,
13952                                NodeKind::LiteralPat {
13953                                    lit: Literal::Int("1".into()),
13954                                },
13955                            )),
13956                            guard: None,
13957                            body: Box::new(block(5, vec![], Some(str_lit(6, "one")))),
13958                        },
13959                    ),
13960                    node(
13961                        7,
13962                        NodeKind::MatchArm {
13963                            pattern: Box::new(node(8, NodeKind::WildcardPat)),
13964                            guard: None,
13965                            body: Box::new(block(9, vec![], Some(str_lit(10, "other")))),
13966                        },
13967                    ),
13968                ],
13969            },
13970        );
13971        let out = gen(&module(vec![], vec![m]));
13972        assert!(out.contains("switch"), "got: {out}");
13973        assert!(out.contains("default:"), "got: {out}");
13974    }
13975
13976    /// A guarded arm now lowers to the shared if/else-if chain: the arm's
13977    /// condition tests the *pattern* AND the *guard* (`x == 1 && (ok)`), so a
13978    /// failed guard falls through to the next arm — the fall-through the prior
13979    /// `case 1: if ok { … }` lowering could not express (its `break` exited the
13980    /// whole switch).
13981    #[test]
13982    fn match_arm_guard_emits_if() {
13983        let m = node(
13984            1,
13985            NodeKind::Match {
13986                scrutinee: Box::new(id_node(2, "x")),
13987                arms: vec![node(
13988                    3,
13989                    NodeKind::MatchArm {
13990                        pattern: Box::new(node(
13991                            4,
13992                            NodeKind::LiteralPat {
13993                                lit: Literal::Int("1".into()),
13994                            },
13995                        )),
13996                        guard: Some(Box::new(id_node(5, "ok"))),
13997                        body: Box::new(block(
13998                            6,
13999                            vec![node(7, NodeKind::Return { value: None })],
14000                            None,
14001                        )),
14002                    },
14003                )],
14004            },
14005        );
14006        let out = gen(&module(vec![], vec![m]));
14007        assert!(
14008            out.contains("if x == 1 && (ok) {"),
14009            "guard should test pattern AND guard in an if-chain, got: {out}"
14010        );
14011        assert!(
14012            !out.contains("switch"),
14013            "a guarded match must not use a switch, got: {out}"
14014        );
14015        assert!(
14016            !out.contains("// guard"),
14017            "guard should not be a comment, got: {out}"
14018        );
14019    }
14020
14021    #[test]
14022    fn let_binding() {
14023        let l = node(
14024            1,
14025            NodeKind::LetBinding {
14026                is_mut: false,
14027                pattern: Box::new(bind_pat(2, "x")),
14028                ty: None,
14029                value: Box::new(int_lit(3, "42")),
14030            },
14031        );
14032        let out = gen(&module(vec![], vec![l]));
14033        // An untyped integer-literal binding is pinned to `int64` (Bock `Int`):
14034        // a bare `x := 42` would be Go's default `int`, which then fails to mix
14035        // with an `int64` value downstream. See the `pin_int64` path.
14036        assert!(out.contains("var x int64 = 42"), "got: {out}");
14037    }
14038
14039    #[test]
14040    fn let_binding_with_type() {
14041        let l = node(
14042            1,
14043            NodeKind::LetBinding {
14044                is_mut: false,
14045                pattern: Box::new(bind_pat(2, "x")),
14046                ty: Some(Box::new(node(
14047                    4,
14048                    NodeKind::TypeNamed {
14049                        path: type_path(&["Int"]),
14050                        args: vec![],
14051                    },
14052                ))),
14053                value: Box::new(int_lit(3, "42")),
14054            },
14055        );
14056        let out = gen(&module(vec![], vec![l]));
14057        assert!(out.contains("var x int64 = 42"), "got: {out}");
14058    }
14059
14060    /// Q-match-exprpos (P4): a value-position `let flag: Bool = match n { … }`
14061    /// inside a function returning `String`. The expression-position match IIFE
14062    /// must take its return type from the *binding's* declared type (`bool`), not
14063    /// the enclosing function's return type (`string`) — otherwise the IIFE
14064    /// (`func() string { … }()`) is not assignable to `var flag bool`. The fix
14065    /// records the declared `let` type as `current_expected_type` and prefers it
14066    /// for the IIFE return.
14067    #[test]
14068    fn expr_position_match_uses_binding_type_not_fn_ret() {
14069        let m = node(
14070            10,
14071            NodeKind::Match {
14072                scrutinee: Box::new(id_node(11, "n")),
14073                arms: vec![
14074                    node(
14075                        12,
14076                        NodeKind::MatchArm {
14077                            pattern: Box::new(node(
14078                                13,
14079                                NodeKind::LiteralPat {
14080                                    lit: Literal::Int("0".into()),
14081                                },
14082                            )),
14083                            guard: None,
14084                            body: Box::new(block(14, vec![], Some(bool_lit(15, true)))),
14085                        },
14086                    ),
14087                    node(
14088                        16,
14089                        NodeKind::MatchArm {
14090                            pattern: Box::new(node(17, NodeKind::WildcardPat)),
14091                            guard: None,
14092                            body: Box::new(block(18, vec![], Some(bool_lit(19, false)))),
14093                        },
14094                    ),
14095                ],
14096            },
14097        );
14098        let let_flag = node(
14099            20,
14100            NodeKind::LetBinding {
14101                is_mut: false,
14102                pattern: Box::new(bind_pat(21, "flag")),
14103                ty: Some(Box::new(node(
14104                    22,
14105                    NodeKind::TypeNamed {
14106                        path: type_path(&["Bool"]),
14107                        args: vec![],
14108                    },
14109                ))),
14110                value: Box::new(m),
14111            },
14112        );
14113        let f = node(
14114            1,
14115            NodeKind::FnDecl {
14116                annotations: vec![],
14117                visibility: Visibility::Public,
14118                is_async: false,
14119                name: ident("decide"),
14120                generic_params: vec![],
14121                params: vec![typed_param_node(2, "n", "Int")],
14122                return_type: Some(Box::new(node(
14123                    3,
14124                    NodeKind::TypeNamed {
14125                        path: type_path(&["String"]),
14126                        args: vec![],
14127                    },
14128                ))),
14129                effect_clause: vec![],
14130                where_clause: vec![],
14131                body: Box::new(block(4, vec![let_flag], Some(str_lit(5, "x")))),
14132            },
14133        );
14134        let out = gen(&module(vec![], vec![f]));
14135        assert!(
14136            out.contains("var flag bool = func() bool {"),
14137            "IIFE must be typed with the binding type (bool), not the fn return (string), got: {out}"
14138        );
14139        assert!(
14140            !out.contains("func() string {"),
14141            "the match IIFE must not be typed with the function return type, got: {out}"
14142        );
14143    }
14144
14145    #[test]
14146    fn if_else() {
14147        let stmt = node(
14148            1,
14149            NodeKind::If {
14150                let_pattern: None,
14151                condition: Box::new(bool_lit(2, true)),
14152                then_block: Box::new(block(3, vec![], Some(int_lit(4, "1")))),
14153                else_block: Some(Box::new(block(5, vec![], Some(int_lit(6, "0"))))),
14154            },
14155        );
14156        let out = gen(&module(vec![], vec![stmt]));
14157        assert!(out.contains("if true {"), "got: {out}");
14158        assert!(out.contains("} else {"), "got: {out}");
14159    }
14160
14161    #[test]
14162    fn for_loop() {
14163        // The loop variable is *referenced* in the body, so it keeps its name.
14164        let stmt = node(
14165            1,
14166            NodeKind::For {
14167                pattern: Box::new(bind_pat(2, "item")),
14168                iterable: Box::new(id_node(3, "items")),
14169                body: Box::new(block(4, vec![id_node(5, "item")], None)),
14170            },
14171        );
14172        let out = gen(&module(vec![], vec![stmt]));
14173        assert!(out.contains("for _, item := range items {"), "got: {out}");
14174    }
14175
14176    #[test]
14177    fn for_loop_unused_var_drops_to_for_range() {
14178        // An unused loop variable would make Go reject `for _, item := range`
14179        // ("declared and not used"); `for _, _ := range` is itself invalid ("no
14180        // new variables on left side of :="). The emitter drops both to the bare
14181        // `for range items` form.
14182        let stmt = node(
14183            1,
14184            NodeKind::For {
14185                pattern: Box::new(bind_pat(2, "item")),
14186                iterable: Box::new(id_node(3, "items")),
14187                body: Box::new(block(4, vec![], None)),
14188            },
14189        );
14190        let out = gen(&module(vec![], vec![stmt]));
14191        assert!(out.contains("for range items {"), "got: {out}");
14192        assert!(!out.contains("for _, item"), "got: {out}");
14193    }
14194
14195    #[test]
14196    fn while_loop() {
14197        let stmt = node(
14198            1,
14199            NodeKind::While {
14200                condition: Box::new(bool_lit(2, true)),
14201                body: Box::new(block(3, vec![], None)),
14202            },
14203        );
14204        let out = gen(&module(vec![], vec![stmt]));
14205        assert!(out.contains("for true {"), "got: {out}");
14206    }
14207
14208    #[test]
14209    fn infinite_loop() {
14210        let stmt = node(
14211            1,
14212            NodeKind::Loop {
14213                body: Box::new(block(2, vec![], None)),
14214            },
14215        );
14216        let out = gen(&module(vec![], vec![stmt]));
14217        assert!(out.contains("for {"), "got: {out}");
14218    }
14219
14220    #[test]
14221    fn string_interpolation() {
14222        let interp = node(
14223            1,
14224            NodeKind::Interpolation {
14225                parts: vec![
14226                    AirInterpolationPart::Literal("Hello, ".into()),
14227                    AirInterpolationPart::Expr(Box::new(id_node(2, "name"))),
14228                    AirInterpolationPart::Literal("!".into()),
14229                ],
14230            },
14231        );
14232        let out = gen(&module(vec![], vec![interp]));
14233        assert!(out.contains("fmt.Sprintf"), "got: {out}");
14234        // Each `${expr}` part renders through `__bockStr` (a `%s` verb over a
14235        // `string`) so a user value with a `Displayable` impl dispatches through
14236        // its `ToString` (Q-displayable-interpolation-dispatch); other values
14237        // fall back to `%v` inside the helper.
14238        assert!(out.contains("Hello, %s!"), "got: {out}");
14239        assert!(out.contains("__bockStr(name)"), "got: {out}");
14240        assert!(out.contains("import \"fmt\""), "got: {out}");
14241    }
14242
14243    /// Q-go-percent-interpolation: a literal `%` inside an interpolated string
14244    /// lands in a `fmt.Sprintf` FORMAT string and must double to `%%` — left
14245    /// single it pairs with the following bytes as a verb (`"${n}% pass"` →
14246    /// `95%!p(MISSING)ass`), a silent cross-target output divergence (the build
14247    /// stays green and only Go corrupts).
14248    #[test]
14249    fn interpolation_escapes_literal_percent() {
14250        let interp = node(
14251            1,
14252            NodeKind::Interpolation {
14253                parts: vec![
14254                    AirInterpolationPart::Expr(Box::new(id_node(2, "n"))),
14255                    AirInterpolationPart::Literal("% pass, 100%% raw".into()),
14256                ],
14257            },
14258        );
14259        let out = gen(&module(vec![], vec![interp]));
14260        // The `${n}` part renders through `__bockStr` (`%s`); the literal `%`
14261        // bytes still double in the format string.
14262        assert!(
14263            out.contains(r#"fmt.Sprintf("%s%% pass, 100%%%% raw", __bockStr(n))"#),
14264            "literal % must be doubled in the Sprintf format string, got: {out}"
14265        );
14266    }
14267
14268    /// Q-go-runtime-helper-shadowing: a parameter named after a public module
14269    /// fn (`lines` — the `core.string` helper shape) must be spelled as the
14270    /// LOCAL (`lines`) at every reference, not the PascalCased helper
14271    /// (`Lines`) — here in for-in iterable position, the dogfood repro.
14272    #[test]
14273    fn local_param_shadows_public_fn_rename() {
14274        let pub_lines = node(
14275            10,
14276            NodeKind::FnDecl {
14277                annotations: vec![],
14278                visibility: Visibility::Public,
14279                is_async: false,
14280                name: ident("lines"),
14281                generic_params: vec![],
14282                params: vec![],
14283                return_type: None,
14284                effect_clause: vec![],
14285                where_clause: vec![],
14286                body: Box::new(block(11, vec![], None)),
14287            },
14288        );
14289        let loop_stmt = node(
14290            20,
14291            NodeKind::For {
14292                pattern: Box::new(bind_pat(21, "line")),
14293                iterable: Box::new(id_node(22, "lines")),
14294                body: Box::new(block(23, vec![id_node(24, "line")], None)),
14295            },
14296        );
14297        let count_fn = node(
14298            30,
14299            NodeKind::FnDecl {
14300                annotations: vec![],
14301                visibility: Visibility::Private,
14302                is_async: false,
14303                name: ident("count"),
14304                generic_params: vec![],
14305                params: vec![param_node(31, "lines")],
14306                return_type: None,
14307                effect_clause: vec![],
14308                where_clause: vec![],
14309                body: Box::new(block(32, vec![loop_stmt], None)),
14310            },
14311        );
14312        let out = gen(&module(vec![], vec![pub_lines, count_fn]));
14313        assert!(
14314            out.contains("range lines {"),
14315            "an in-scope param must shadow the public-fn rename, got: {out}"
14316        );
14317        assert!(
14318            !out.contains("range Lines"),
14319            "the PascalCased helper must not be referenced for the local, got: {out}"
14320        );
14321    }
14322
14323    /// Q-go-split-combinator-typing: the builtin String-method return table
14324    /// mirrors `try_emit_string_method`'s lowerings (`split` → `[]string`,
14325    /// transforms → `string`, …), gated on the checker's receiver-kind
14326    /// annotation so a same-named user method is never mistaken for the
14327    /// builtin.
14328    #[test]
14329    fn string_builtin_return_type_mirrors_lowering() {
14330        let build = |method: &str, tag: Option<&str>| {
14331            let object = id_node(5, "s");
14332            let callee = node(
14333                6,
14334                NodeKind::FieldAccess {
14335                    object: Box::new(object),
14336                    field: ident(method),
14337                },
14338            );
14339            // The lowerer clones the receiver into the leading self arg: same
14340            // NodeId as the field-access object (see `desugared_self_call`).
14341            let args = vec![
14342                AirArg {
14343                    label: None,
14344                    value: id_node(5, "s"),
14345                },
14346                AirArg {
14347                    label: None,
14348                    value: str_lit(7, ","),
14349                },
14350            ];
14351            let mut call = node(
14352                8,
14353                NodeKind::Call {
14354                    callee: Box::new(callee.clone()),
14355                    args: args.clone(),
14356                    type_args: vec![],
14357                },
14358            );
14359            if let Some(tag) = tag {
14360                call.metadata.insert(
14361                    bock_types::checker::RECV_KIND_META_KEY.to_string(),
14362                    bock_air::Value::String(tag.to_string()),
14363                );
14364            }
14365            (call, callee, args)
14366        };
14367        for (method, expect) in [
14368            ("split", Some("[]string")),
14369            ("trim", Some("string")),
14370            ("to_upper", Some("string")),
14371            ("len", Some("int64")),
14372            ("contains", Some("bool")),
14373            ("char_at", Some("__bockOption")),
14374            ("frobnicate", None),
14375        ] {
14376            let (call, callee, args) = build(method, Some("Primitive:String"));
14377            assert_eq!(
14378                GoEmitCtx::string_builtin_return_go_type(&call, &callee, &args).as_deref(),
14379                expect,
14380                "method {method}"
14381            );
14382        }
14383        // No checker annotation / non-String receiver → not the builtin.
14384        let (call, callee, args) = build("split", None);
14385        assert_eq!(
14386            GoEmitCtx::string_builtin_return_go_type(&call, &callee, &args),
14387            None
14388        );
14389        let (call, callee, args) = build("split", Some("User:Tokenizer"));
14390        assert_eq!(
14391            GoEmitCtx::string_builtin_return_go_type(&call, &callee, &args),
14392            None
14393        );
14394    }
14395
14396    #[test]
14397    fn record_construction() {
14398        let rc = node(
14399            1,
14400            NodeKind::RecordConstruct {
14401                path: type_path(&["Point"]),
14402                fields: vec![
14403                    AirRecordField {
14404                        name: ident("x"),
14405                        value: Some(Box::new(int_lit(2, "1"))),
14406                    },
14407                    AirRecordField {
14408                        name: ident("y"),
14409                        value: Some(Box::new(int_lit(3, "2"))),
14410                    },
14411                ],
14412                spread: None,
14413            },
14414        );
14415        let out = gen(&module(vec![], vec![rc]));
14416        assert!(out.contains("Point{X: 1, Y: 2}"), "got: {out}");
14417    }
14418
14419    #[test]
14420    fn list_literal() {
14421        let l = node(
14422            1,
14423            NodeKind::ListLiteral {
14424                elems: vec![int_lit(2, "1"), int_lit(3, "2"), int_lit(4, "3")],
14425            },
14426        );
14427        let out = gen(&module(vec![], vec![l]));
14428        // A homogeneous integer list literal now infers a concrete element
14429        // type (`[]int64`), not the erased `[]interface{}` — so element
14430        // arithmetic / typed iteration / typed returns compile (P3-α item 1b).
14431        assert!(out.contains("[]int64{1, 2, 3}"), "got: {out}");
14432    }
14433
14434    /// A list literal with no concretely-inferable common element type (here a
14435    /// mixed int/string literal) falls back to the erased `[]interface{}` —
14436    /// never a wrong concrete type (P3-α item 1b).
14437    #[test]
14438    fn list_literal_mixed_falls_back_to_interface() {
14439        let l = node(
14440            1,
14441            NodeKind::ListLiteral {
14442                elems: vec![int_lit(2, "1"), str_lit(3, "x")],
14443            },
14444        );
14445        let out = gen(&module(vec![], vec![l]));
14446        assert!(out.contains("[]interface{}{1, \"x\"}"), "got: {out}");
14447    }
14448
14449    /// An empty list literal cannot infer an element type, so it falls back to
14450    /// `[]interface{}` when emitted with no declared-type context.
14451    #[test]
14452    fn empty_list_literal_falls_back_to_interface() {
14453        let l = node(1, NodeKind::ListLiteral { elems: vec![] });
14454        let out = gen(&module(vec![], vec![l]));
14455        assert!(out.contains("[]interface{}{}"), "got: {out}");
14456    }
14457
14458    /// A homogeneous map literal infers its key and value element types
14459    /// separately (`map[string]int64`), not the erased
14460    /// `map[interface{}]interface{}` (P3-α item 1b).
14461    #[test]
14462    fn map_literal_infers_key_and_value() {
14463        let entry = AirMapEntry {
14464            key: str_lit(2, "a"),
14465            value: int_lit(3, "1"),
14466        };
14467        let m = node(
14468            1,
14469            NodeKind::MapLiteral {
14470                entries: vec![entry],
14471            },
14472        );
14473        let out = gen(&module(vec![], vec![m]));
14474        assert!(out.contains("map[string]int64{\"a\": 1}"), "got: {out}");
14475    }
14476
14477    /// A homogeneous set literal infers a concrete element type
14478    /// (`map[int64]struct{}`).
14479    #[test]
14480    fn set_literal_infers_elem() {
14481        let s = node(
14482            1,
14483            NodeKind::SetLiteral {
14484                elems: vec![int_lit(2, "1"), int_lit(3, "2")],
14485            },
14486        );
14487        let out = gen(&module(vec![], vec![s]));
14488        assert!(
14489            out.contains("map[int64]struct{}{1: {}, 2: {}}"),
14490            "got: {out}"
14491        );
14492    }
14493
14494    #[test]
14495    fn effect_decl_to_interface() {
14496        let ed = node(
14497            1,
14498            NodeKind::EffectDecl {
14499                annotations: vec![],
14500                visibility: Visibility::Public,
14501                name: ident("Logger"),
14502                generic_params: vec![],
14503                components: vec![],
14504                operations: vec![
14505                    node(
14506                        2,
14507                        NodeKind::FnDecl {
14508                            annotations: vec![],
14509                            visibility: Visibility::Public,
14510                            is_async: false,
14511                            name: ident("info"),
14512                            generic_params: vec![],
14513                            params: vec![typed_param_node(3, "msg", "String")],
14514                            return_type: None,
14515                            effect_clause: vec![],
14516                            where_clause: vec![],
14517                            body: Box::new(block(4, vec![], None)),
14518                        },
14519                    ),
14520                    node(
14521                        5,
14522                        NodeKind::FnDecl {
14523                            annotations: vec![],
14524                            visibility: Visibility::Public,
14525                            is_async: false,
14526                            name: ident("error"),
14527                            generic_params: vec![],
14528                            params: vec![typed_param_node(6, "msg", "String")],
14529                            return_type: None,
14530                            effect_clause: vec![],
14531                            where_clause: vec![],
14532                            body: Box::new(block(7, vec![], None)),
14533                        },
14534                    ),
14535                ],
14536            },
14537        );
14538        let out = gen(&module(vec![], vec![ed]));
14539        assert!(out.contains("type Logger interface {"), "got: {out}");
14540        assert!(out.contains("Info(string)"), "got: {out}");
14541        assert!(out.contains("Error(string)"), "got: {out}");
14542    }
14543
14544    #[test]
14545    fn result_construct_ok() {
14546        // `ResultConstruct` lowers to the tagged Result-runtime constructor
14547        // `__bockOk(..)` — the same shape the surface `Ok(..)` construction emits
14548        // and the `Result` match reads — reconciling construction with match. A
14549        // numeric-*literal* payload is boxed at its concrete Go type
14550        // (`int64(42)`), so the `interface{}` box's dynamic type is `int64`, not
14551        // the untyped-constant default `int` — a later `.(int64)` / generic
14552        // `.(T)` payload assertion would otherwise panic (`box_payload_str`).
14553        let rc = node(
14554            1,
14555            NodeKind::ResultConstruct {
14556                variant: ResultVariant::Ok,
14557                value: Some(Box::new(int_lit(2, "42"))),
14558            },
14559        );
14560        let out = gen(&module(vec![], vec![rc]));
14561        assert!(out.contains("__bockOk(int64(42))"), "got: {out}");
14562    }
14563
14564    #[test]
14565    fn result_construct_err() {
14566        let rc = node(
14567            1,
14568            NodeKind::ResultConstruct {
14569                variant: ResultVariant::Err,
14570                value: Some(Box::new(str_lit(2, "failed"))),
14571            },
14572        );
14573        let out = gen(&module(vec![], vec![rc]));
14574        // A string payload is *not* boxed — only numeric literals are cast.
14575        assert!(out.contains("__bockErr(\"failed\")"), "got: {out}");
14576    }
14577
14578    #[test]
14579    fn numeric_literal_go_type_recognises_int_float_and_negation() {
14580        // Bare int / float literals carry their concrete Go boxing type; a unary
14581        // negation is transparent; a string / identifier is not numeric.
14582        assert_eq!(
14583            GoEmitCtx::numeric_literal_go_type(&int_lit(1, "7")),
14584            Some("int64")
14585        );
14586        let flt = node(
14587            2,
14588            NodeKind::Literal {
14589                lit: Literal::Float("1.5".into()),
14590            },
14591        );
14592        assert_eq!(GoEmitCtx::numeric_literal_go_type(&flt), Some("float64"));
14593        let neg = node(
14594            3,
14595            NodeKind::UnaryOp {
14596                op: UnaryOp::Neg,
14597                operand: Box::new(int_lit(4, "1")),
14598            },
14599        );
14600        assert_eq!(GoEmitCtx::numeric_literal_go_type(&neg), Some("int64"));
14601        assert_eq!(
14602            GoEmitCtx::numeric_literal_go_type(&str_lit(5, "x")),
14603            None,
14604            "a string literal is not a numeric payload"
14605        );
14606        assert_eq!(
14607            GoEmitCtx::numeric_literal_go_type(&id_node(6, "v")),
14608            None,
14609            "a variable reference is not a numeric literal"
14610        );
14611    }
14612
14613    #[test]
14614    fn rendered_collection_elem_parses_slice_and_map() {
14615        // A collection-typed IIFE return propagates its element type(s) to the
14616        // arm-body literals; the rendering is parsed back from the Go type string.
14617        assert_eq!(
14618            GoEmitCtx::rendered_collection_elem("[]int64"),
14619            Some(("int64".to_string(), None))
14620        );
14621        assert_eq!(
14622            GoEmitCtx::rendered_collection_elem("map[string]int64"),
14623            Some(("string".to_string(), Some("int64".to_string())))
14624        );
14625        // A nested map value keeps its inner brackets balanced.
14626        assert_eq!(
14627            GoEmitCtx::rendered_collection_elem("map[string][]int64"),
14628            Some(("string".to_string(), Some("[]int64".to_string())))
14629        );
14630        // A non-collection type yields nothing (the IIFE stays scalar-typed).
14631        assert_eq!(GoEmitCtx::rendered_collection_elem("__bockOption"), None);
14632        assert_eq!(GoEmitCtx::rendered_collection_elem("int64"), None);
14633    }
14634
14635    #[test]
14636    fn class_to_struct_with_methods() {
14637        let cls = node(
14638            1,
14639            NodeKind::ClassDecl {
14640                annotations: vec![],
14641                visibility: Visibility::Public,
14642                name: ident("Counter"),
14643                generic_params: vec![],
14644                base: None,
14645                traits: vec![],
14646                fields: vec![bock_ast::RecordDeclField {
14647                    id: 0,
14648                    span: span(),
14649                    name: ident("count"),
14650                    ty: TypeExpr::Named {
14651                        id: 0,
14652                        span: span(),
14653                        path: type_path(&["Int"]),
14654                        args: vec![],
14655                    },
14656                    default: None,
14657                }],
14658                methods: vec![node(
14659                    2,
14660                    NodeKind::FnDecl {
14661                        annotations: vec![],
14662                        visibility: Visibility::Public,
14663                        is_async: false,
14664                        name: ident("increment"),
14665                        generic_params: vec![],
14666                        // Instance method leads with `self` (real lowering).
14667                        params: vec![param_node(4, "self")],
14668                        return_type: None,
14669                        effect_clause: vec![],
14670                        where_clause: vec![],
14671                        body: Box::new(block(3, vec![], None)),
14672                    },
14673                )],
14674            },
14675        );
14676        let out = gen(&module(vec![], vec![cls]));
14677        assert!(out.contains("type Counter struct {"), "got: {out}");
14678        assert!(out.contains("Count\tint64"), "got: {out}");
14679        assert!(out.contains("func NewCounter("), "got: {out}");
14680        assert!(
14681            out.contains("func (self *Counter) Increment()"),
14682            "got: {out}"
14683        );
14684    }
14685
14686    #[test]
14687    fn lambda_expression() {
14688        let lam = node(
14689            1,
14690            NodeKind::Lambda {
14691                params: vec![param_node(2, "x")],
14692                body: Box::new(node(
14693                    3,
14694                    NodeKind::BinaryOp {
14695                        op: BinOp::Mul,
14696                        left: Box::new(id_node(4, "x")),
14697                        right: Box::new(int_lit(5, "2")),
14698                    },
14699                )),
14700            },
14701        );
14702        let out = gen(&module(vec![], vec![lam]));
14703        assert!(
14704            out.contains("func(x interface{}) interface{} { return (x * 2) }"),
14705            "got: {out}"
14706        );
14707    }
14708
14709    #[test]
14710    fn impl_block_methods() {
14711        let imp = node(
14712            1,
14713            NodeKind::ImplBlock {
14714                annotations: vec![],
14715                generic_params: vec![],
14716                trait_path: None,
14717                trait_args: vec![],
14718                target: Box::new(node(
14719                    2,
14720                    NodeKind::TypeNamed {
14721                        path: type_path(&["Point"]),
14722                        args: vec![],
14723                    },
14724                )),
14725                where_clause: vec![],
14726                methods: vec![node(
14727                    3,
14728                    NodeKind::FnDecl {
14729                        annotations: vec![],
14730                        visibility: Visibility::Public,
14731                        is_async: false,
14732                        name: ident("distance"),
14733                        generic_params: vec![],
14734                        // Instance method leads with `self`; a no-`self` method is
14735                        // an associated function (emitted as a free function).
14736                        params: vec![param_node(7, "self")],
14737                        return_type: Some(Box::new(node(
14738                            4,
14739                            NodeKind::TypeNamed {
14740                                path: type_path(&["Float"]),
14741                                args: vec![],
14742                            },
14743                        ))),
14744                        effect_clause: vec![],
14745                        where_clause: vec![],
14746                        body: Box::new(block(5, vec![], Some(int_lit(6, "0")))),
14747                    },
14748                )],
14749            },
14750        );
14751        let out = gen(&module(vec![], vec![imp]));
14752        assert!(
14753            out.contains("func (self *Point) Distance() float64 {"),
14754            "got: {out}"
14755        );
14756    }
14757
14758    /// A plain inherent `impl` method that names `Self` in its return type must
14759    /// resolve `Self` to the receiver type (`Point`), not the `/* Self */`
14760    /// placeholder. Before P3-α item 6-go-self, `go_self_subst` was set only for
14761    /// trait impls (value receivers), so an inherent-impl `Self` lowered to the
14762    /// placeholder and produced an invalid Go signature.
14763    #[test]
14764    fn self_in_plain_impl_resolves_to_receiver_type() {
14765        let imp = node(
14766            1,
14767            NodeKind::ImplBlock {
14768                annotations: vec![],
14769                generic_params: vec![],
14770                trait_path: None,
14771                trait_args: vec![],
14772                target: Box::new(node(
14773                    2,
14774                    NodeKind::TypeNamed {
14775                        path: type_path(&["Point"]),
14776                        args: vec![],
14777                    },
14778                )),
14779                where_clause: vec![],
14780                methods: vec![node(
14781                    3,
14782                    NodeKind::FnDecl {
14783                        annotations: vec![],
14784                        visibility: Visibility::Public,
14785                        is_async: false,
14786                        name: ident("clone"),
14787                        generic_params: vec![],
14788                        // Instance method leads with `self` (real lowering).
14789                        params: vec![param_node(6, "self")],
14790                        return_type: Some(Box::new(node(4, NodeKind::TypeSelf))),
14791                        effect_clause: vec![],
14792                        where_clause: vec![],
14793                        body: Box::new(block(5, vec![], None)),
14794                    },
14795                )],
14796            },
14797        );
14798        let out = gen(&module(vec![], vec![imp]));
14799        assert!(
14800            out.contains("func (self *Point) Clone() Point {"),
14801            "Self should resolve to the receiver type Point, got: {out}"
14802        );
14803        assert!(
14804            !out.contains("/* Self */"),
14805            "Self placeholder must not leak, got: {out}"
14806        );
14807    }
14808
14809    /// A record construction with a spread base (`Point { y: 9, ..p }`) lowers
14810    /// to a copy-then-override IIFE — Go has no struct-spread syntax — rather
14811    /// than dropping the `..p` base (P3-α item 5).
14812    #[test]
14813    fn record_spread_lowers_to_iife() {
14814        let spread_base = id_node(10, "p");
14815        let rc = node(
14816            1,
14817            NodeKind::RecordConstruct {
14818                path: type_path(&["Point"]),
14819                fields: vec![AirRecordField {
14820                    name: ident("y"),
14821                    value: Some(Box::new(int_lit(2, "9"))),
14822                }],
14823                spread: Some(Box::new(spread_base)),
14824            },
14825        );
14826        let out = gen(&module(vec![], vec![rc]));
14827        assert!(
14828            out.contains("func() Point { __s := p; __s.Y = 9; return __s }()"),
14829            "spread should copy base then override, got: {out}"
14830        );
14831        assert!(
14832            !out.contains("/* spread */"),
14833            "the dropped-spread TODO must be gone, got: {out}"
14834        );
14835    }
14836
14837    #[test]
14838    fn concurrency_goroutine() {
14839        // Async function → goroutine pattern with channel.
14840        // The await expression maps to channel receive.
14841        let body = block(
14842            3,
14843            vec![],
14844            Some(node(
14845                4,
14846                NodeKind::Await {
14847                    expr: Box::new(id_node(5, "ch")),
14848                },
14849            )),
14850        );
14851        let f = node(
14852            1,
14853            NodeKind::FnDecl {
14854                annotations: vec![],
14855                visibility: Visibility::Public,
14856                is_async: true,
14857                name: ident("fetchData"),
14858                generic_params: vec![],
14859                params: vec![],
14860                return_type: None,
14861                effect_clause: vec![],
14862                where_clause: vec![],
14863                body: Box::new(body),
14864            },
14865        );
14866        let out = gen(&module(vec![], vec![f]));
14867        assert!(out.contains("func FetchData()"), "got: {out}");
14868        assert!(out.contains("<-ch"), "got: {out}");
14869    }
14870
14871    #[test]
14872    fn async_fn_emits_goroutine_wrapper() {
14873        // Async function with Int return → sync body + FnAsync wrapper
14874        // returning `<-chan int`.
14875        let body = block(3, vec![], Some(int_lit(4, "42")));
14876        let f = node(
14877            1,
14878            NodeKind::FnDecl {
14879                annotations: vec![],
14880                visibility: Visibility::Public,
14881                is_async: true,
14882                name: ident("task1"),
14883                generic_params: vec![],
14884                params: vec![],
14885                return_type: Some(Box::new(node(
14886                    5,
14887                    NodeKind::TypeNamed {
14888                        path: type_path(&["Int"]),
14889                        args: vec![],
14890                    },
14891                ))),
14892                effect_clause: vec![],
14893                where_clause: vec![],
14894                body: Box::new(body),
14895            },
14896        );
14897        let out = gen(&module(vec![], vec![f]));
14898        assert!(
14899            out.contains("func Task1() int64 {"),
14900            "sync body missing: {out}"
14901        );
14902        assert!(
14903            out.contains("func Task1Async() <-chan int64 {"),
14904            "async wrapper missing: {out}"
14905        );
14906        assert!(out.contains("__ch := make(chan int64, 1)"), "got: {out}");
14907        assert!(out.contains("go func() {"), "got: {out}");
14908        assert!(out.contains("__ch <- Task1()"), "got: {out}");
14909        assert!(out.contains("return __ch"), "got: {out}");
14910    }
14911
14912    /// A `public fn main` must still emit Go\'s entry `func main()`, not the
14913    /// PascalCased `func Main()` (codegen-correctness defect 6).
14914    #[test]
14915    fn public_main_emits_entry_point() {
14916        let f = node(
14917            1,
14918            NodeKind::FnDecl {
14919                annotations: vec![],
14920                visibility: Visibility::Public,
14921                is_async: false,
14922                name: ident("main"),
14923                generic_params: vec![],
14924                params: vec![],
14925                return_type: None,
14926                effect_clause: vec![],
14927                where_clause: vec![],
14928                body: Box::new(block(2, vec![], None)),
14929            },
14930        );
14931        let out = gen(&module(vec![], vec![f]));
14932        assert!(out.contains("func main() {"), "got: {out}");
14933        assert!(!out.contains("func Main"), "got: {out}");
14934    }
14935
14936    /// The Optional runtime prelude is emitted only when the module uses
14937    /// `Optional`/`Some`/`None` (codegen-correctness defect 4).
14938    #[test]
14939    fn optional_runtime_gated_on_use() {
14940        // A module that constructs `Some`/`None` pulls in the runtime.
14941        let some_call = node(
14942            10,
14943            NodeKind::Call {
14944                callee: Box::new(id_node(11, "Some")),
14945                args: vec![AirArg {
14946                    label: None,
14947                    value: int_lit(12, "1"),
14948                }],
14949                type_args: vec![],
14950            },
14951        );
14952        let f = node(
14953            1,
14954            NodeKind::FnDecl {
14955                annotations: vec![],
14956                visibility: Visibility::Private,
14957                is_async: false,
14958                name: ident("main"),
14959                generic_params: vec![],
14960                params: vec![],
14961                return_type: None,
14962                effect_clause: vec![],
14963                where_clause: vec![],
14964                body: Box::new(block(2, vec![some_call], None)),
14965            },
14966        );
14967        let out = gen(&module(vec![], vec![f]));
14968        assert!(out.contains("type __bockOption struct"), "got: {out}");
14969        assert!(out.contains("__bockSome("), "got: {out}");
14970
14971        // A module that does not mention Optional gets no prelude.
14972        let f2 = node(
14973            1,
14974            NodeKind::FnDecl {
14975                annotations: vec![],
14976                visibility: Visibility::Private,
14977                is_async: false,
14978                name: ident("main"),
14979                generic_params: vec![],
14980                params: vec![],
14981                return_type: None,
14982                effect_clause: vec![],
14983                where_clause: vec![],
14984                body: Box::new(block(2, vec![], None)),
14985            },
14986        );
14987        let out2 = gen(&module(vec![], vec![f2]));
14988        assert!(!out2.contains("__bockOption"), "got: {out2}");
14989    }
14990
14991    #[test]
14992    fn result_runtime_gated_and_constructed() {
14993        // A module that constructs `Ok`/`Err` pulls in the Result runtime + the
14994        // shared numeric helpers, and lowers the constructors to `__bockOk`/
14995        // `__bockErr` (not the old `v, nil` multi-return).
14996        let ok_call = node(
14997            10,
14998            NodeKind::Call {
14999                callee: Box::new(id_node(11, "Ok")),
15000                args: vec![AirArg {
15001                    label: None,
15002                    value: int_lit(12, "7"),
15003                }],
15004                type_args: vec![],
15005            },
15006        );
15007        let f = node(
15008            1,
15009            NodeKind::FnDecl {
15010                annotations: vec![],
15011                visibility: Visibility::Private,
15012                is_async: false,
15013                name: ident("main"),
15014                generic_params: vec![],
15015                params: vec![],
15016                return_type: None,
15017                effect_clause: vec![],
15018                where_clause: vec![],
15019                body: Box::new(block(2, vec![ok_call], None)),
15020            },
15021        );
15022        let out = gen(&module(vec![], vec![f]));
15023        assert!(out.contains("type __bockResult struct"), "got: {out}");
15024        assert!(out.contains("__bockOk("), "got: {out}");
15025        // The shared numeric helpers are emitted exactly once.
15026        assert_eq!(
15027            out.matches("func __bockAsInt64").count(),
15028            1,
15029            "numeric helpers must be emitted once; got: {out}"
15030        );
15031    }
15032
15033    /// The Go Optional runtime stores the `Some` payload as `interface{}`. A
15034    /// `match` arm binding it (`Some(x)`) must type-assert to the scrutinee's
15035    /// concrete element type so typed use (`x + 10`) compiles. The element type
15036    /// is resolved structurally from the `Optional[T]` parameter scrutinee.
15037    #[test]
15038    fn optional_match_some_payload_type_asserted() {
15039        // fn addTen(o: Int?) -> Int { match o { Some(x) => return x; None => return 0 } }
15040        let opt_int_ty = node(
15041            200,
15042            NodeKind::TypeOptional {
15043                inner: Box::new(node(
15044                    201,
15045                    NodeKind::TypeNamed {
15046                        path: type_path(&["Int"]),
15047                        args: vec![],
15048                    },
15049                )),
15050            },
15051        );
15052        let o_param = node(
15053            30,
15054            NodeKind::Param {
15055                pattern: Box::new(bind_pat(31, "o")),
15056                ty: Some(Box::new(opt_int_ty)),
15057                default: None,
15058            },
15059        );
15060        let some_arm = node(
15061            40,
15062            NodeKind::MatchArm {
15063                pattern: Box::new(node(
15064                    41,
15065                    NodeKind::ConstructorPat {
15066                        path: type_path(&["Some"]),
15067                        fields: vec![bind_pat(42, "x")],
15068                    },
15069                )),
15070                guard: None,
15071                body: Box::new(block(
15072                    43,
15073                    vec![node(
15074                        44,
15075                        NodeKind::Return {
15076                            value: Some(Box::new(id_node(45, "x"))),
15077                        },
15078                    )],
15079                    None,
15080                )),
15081            },
15082        );
15083        let none_arm = node(
15084            50,
15085            NodeKind::MatchArm {
15086                pattern: Box::new(node(
15087                    51,
15088                    NodeKind::ConstructorPat {
15089                        path: type_path(&["None"]),
15090                        fields: vec![],
15091                    },
15092                )),
15093                guard: None,
15094                body: Box::new(block(
15095                    52,
15096                    vec![node(
15097                        53,
15098                        NodeKind::Return {
15099                            value: Some(Box::new(int_lit(54, "0"))),
15100                        },
15101                    )],
15102                    None,
15103                )),
15104            },
15105        );
15106        let match_stmt = node(
15107            60,
15108            NodeKind::Match {
15109                scrutinee: Box::new(id_node(61, "o")),
15110                arms: vec![some_arm, none_arm],
15111            },
15112        );
15113        let f = node(
15114            1,
15115            NodeKind::FnDecl {
15116                annotations: vec![],
15117                visibility: Visibility::Private,
15118                is_async: false,
15119                name: ident("addTen"),
15120                generic_params: vec![],
15121                params: vec![o_param],
15122                return_type: Some(Box::new(node(
15123                    2,
15124                    NodeKind::TypeNamed {
15125                        path: type_path(&["Int"]),
15126                        args: vec![],
15127                    },
15128                ))),
15129                effect_clause: vec![],
15130                where_clause: vec![],
15131                body: Box::new(block(3, vec![match_stmt], None)),
15132            },
15133        );
15134        let out = gen(&module(vec![], vec![f]));
15135        // The `Int` element type is recovered through the widening helper
15136        // `__bockAsInt64` rather than a hard `.(int64)` assertion: a payload
15137        // boxed from an untyped Go constant (`Some(10)`) is a Go `int`, on which
15138        // `.(int64)` panics at runtime.
15139        assert!(
15140            out.contains("x := __bockAsInt64(__opt.v);"),
15141            "Some payload should be recovered via the int64 widening helper, got: {out}"
15142        );
15143    }
15144
15145    /// Build an `impl Counter { fn next(self) -> Int? { ... } }` whose method
15146    /// has an `Optional[Int]` return type, plus a `match it.next() { Some(x) =>
15147    /// return x; None => return 0 }` driver function. Used to exercise the
15148    /// method-call-scrutinee payload resolution (the `core.iter` desugar shape).
15149    fn iterator_module_with_method_match() -> AIRNode {
15150        let opt_int_ty = node(
15151            200,
15152            NodeKind::TypeOptional {
15153                inner: Box::new(node(
15154                    201,
15155                    NodeKind::TypeNamed {
15156                        path: type_path(&["Int"]),
15157                        args: vec![],
15158                    },
15159                )),
15160            },
15161        );
15162        let next_method = node(
15163            10,
15164            NodeKind::FnDecl {
15165                annotations: vec![],
15166                visibility: Visibility::Private,
15167                is_async: false,
15168                name: ident("next"),
15169                generic_params: vec![],
15170                params: vec![param_node(11, "self")],
15171                return_type: Some(Box::new(opt_int_ty)),
15172                effect_clause: vec![],
15173                where_clause: vec![],
15174                body: Box::new(block(
15175                    12,
15176                    vec![node(
15177                        13,
15178                        NodeKind::Return {
15179                            value: Some(Box::new(node(
15180                                14,
15181                                NodeKind::Call {
15182                                    callee: Box::new(id_node(15, "None")),
15183                                    args: vec![],
15184                                    type_args: vec![],
15185                                },
15186                            ))),
15187                        },
15188                    )],
15189                    None,
15190                )),
15191            },
15192        );
15193        let imp = node(
15194            5,
15195            NodeKind::ImplBlock {
15196                annotations: vec![],
15197                generic_params: vec![],
15198                trait_path: None,
15199                trait_args: vec![],
15200                target: Box::new(node(
15201                    6,
15202                    NodeKind::TypeNamed {
15203                        path: type_path(&["Counter"]),
15204                        args: vec![],
15205                    },
15206                )),
15207                where_clause: vec![],
15208                methods: vec![next_method],
15209            },
15210        );
15211        // fn drive(it: Counter) -> Int {
15212        //   match it.next() { Some(x) => return x; None => return 0 }
15213        // }
15214        let scrutinee = node(
15215            60,
15216            NodeKind::MethodCall {
15217                receiver: Box::new(id_node(61, "it")),
15218                method: ident("next"),
15219                type_args: vec![],
15220                args: vec![],
15221            },
15222        );
15223        let some_arm = node(
15224            40,
15225            NodeKind::MatchArm {
15226                pattern: Box::new(node(
15227                    41,
15228                    NodeKind::ConstructorPat {
15229                        path: type_path(&["Some"]),
15230                        fields: vec![bind_pat(42, "x")],
15231                    },
15232                )),
15233                guard: None,
15234                body: Box::new(block(
15235                    43,
15236                    vec![node(
15237                        44,
15238                        NodeKind::Return {
15239                            value: Some(Box::new(id_node(45, "x"))),
15240                        },
15241                    )],
15242                    None,
15243                )),
15244            },
15245        );
15246        let none_arm = node(
15247            50,
15248            NodeKind::MatchArm {
15249                pattern: Box::new(node(
15250                    51,
15251                    NodeKind::ConstructorPat {
15252                        path: type_path(&["None"]),
15253                        fields: vec![],
15254                    },
15255                )),
15256                guard: None,
15257                body: Box::new(block(
15258                    52,
15259                    vec![node(
15260                        53,
15261                        NodeKind::Return {
15262                            value: Some(Box::new(int_lit(54, "0"))),
15263                        },
15264                    )],
15265                    None,
15266                )),
15267            },
15268        );
15269        let match_stmt = node(
15270            70,
15271            NodeKind::Match {
15272                scrutinee: Box::new(scrutinee),
15273                arms: vec![some_arm, none_arm],
15274            },
15275        );
15276        let drive = node(
15277            80,
15278            NodeKind::FnDecl {
15279                annotations: vec![],
15280                visibility: Visibility::Private,
15281                is_async: false,
15282                name: ident("drive"),
15283                generic_params: vec![],
15284                params: vec![typed_param_node(81, "it", "Counter")],
15285                return_type: Some(Box::new(node(
15286                    82,
15287                    NodeKind::TypeNamed {
15288                        path: type_path(&["Int"]),
15289                        args: vec![],
15290                    },
15291                ))),
15292                effect_clause: vec![],
15293                where_clause: vec![],
15294                body: Box::new(block(83, vec![match_stmt], None)),
15295            },
15296        );
15297        module(vec![], vec![imp, drive])
15298    }
15299
15300    #[test]
15301    fn optional_match_method_call_scrutinee_payload_resolved() {
15302        // The scrutinee `it.next()` is a method call whose method returns
15303        // `Int?`; the bound `Some` payload must be recovered as `int64` (via the
15304        // widening helper), not left as bare `interface{}`. This is the
15305        // `core.iter` `for x in <Iterable>` desugar shape — regression-locking
15306        // the Go method-call-scrutinee defect.
15307        let out = gen(&iterator_module_with_method_match());
15308        assert!(
15309            out.contains("x := __bockAsInt64(__opt.v);"),
15310            "method-call-scrutinee Some payload should be resolved to int64, got: {out}"
15311        );
15312    }
15313
15314    /// Build a `loop { match it.next() { Some(x) => { ... } None => break } }`
15315    /// driver — the exact statement-position desugar shape, where the 2-arm
15316    /// Optional match lowers to `if/else` and a bare `break` already exits the
15317    /// `for`. No loop label may be allocated (Go rejects an unused label).
15318    fn loop_with_optional_match_break() -> AIRNode {
15319        let scrutinee = node(
15320            60,
15321            NodeKind::MethodCall {
15322                receiver: Box::new(id_node(61, "it")),
15323                method: ident("next"),
15324                type_args: vec![],
15325                args: vec![],
15326            },
15327        );
15328        let some_arm = node(
15329            40,
15330            NodeKind::MatchArm {
15331                pattern: Box::new(node(
15332                    41,
15333                    NodeKind::ConstructorPat {
15334                        path: type_path(&["Some"]),
15335                        fields: vec![bind_pat(42, "x")],
15336                    },
15337                )),
15338                guard: None,
15339                // Some(x) => { sum = sum + x } — a statement-style arm body.
15340                body: Box::new(block(
15341                    43,
15342                    vec![node(
15343                        44,
15344                        NodeKind::Assign {
15345                            op: AssignOp::Assign,
15346                            target: Box::new(id_node(45, "sum")),
15347                            value: Box::new(node(
15348                                46,
15349                                NodeKind::BinaryOp {
15350                                    op: BinOp::Add,
15351                                    left: Box::new(id_node(47, "sum")),
15352                                    right: Box::new(id_node(48, "x")),
15353                                },
15354                            )),
15355                        },
15356                    )],
15357                    None,
15358                )),
15359            },
15360        );
15361        let none_arm = node(
15362            50,
15363            NodeKind::MatchArm {
15364                pattern: Box::new(node(
15365                    51,
15366                    NodeKind::ConstructorPat {
15367                        path: type_path(&["None"]),
15368                        fields: vec![],
15369                    },
15370                )),
15371                guard: None,
15372                body: Box::new(block(
15373                    52,
15374                    vec![node(53, NodeKind::Break { value: None })],
15375                    None,
15376                )),
15377            },
15378        );
15379        let match_stmt = node(
15380            70,
15381            NodeKind::Match {
15382                scrutinee: Box::new(scrutinee),
15383                arms: vec![some_arm, none_arm],
15384            },
15385        );
15386        let loop_node = node(
15387            71,
15388            NodeKind::Loop {
15389                body: Box::new(block(72, vec![match_stmt], None)),
15390            },
15391        );
15392        let f = node(
15393            80,
15394            NodeKind::FnDecl {
15395                annotations: vec![],
15396                visibility: Visibility::Private,
15397                is_async: false,
15398                name: ident("run"),
15399                generic_params: vec![],
15400                params: vec![],
15401                return_type: None,
15402                effect_clause: vec![],
15403                where_clause: vec![],
15404                body: Box::new(block(81, vec![loop_node], None)),
15405            },
15406        );
15407        module(vec![], vec![f])
15408    }
15409
15410    #[test]
15411    fn optional_match_break_loop_has_no_unused_label() {
15412        // A 2-arm Some/None match lowers to `if __opt.tag == "Some" { ... } else
15413        // { break }`; the bare `break` already exits the `for`, so no
15414        // `__bockLoopN:` label must be emitted (Go errors on an unused label).
15415        let out = gen(&loop_with_optional_match_break());
15416        assert!(
15417            !out.contains("__bockLoop"),
15418            "Optional match-in-loop must not allocate an unused loop label, got: {out}"
15419        );
15420        // The bare `break` is still present and targets the enclosing `for`.
15421        assert!(out.contains("break"), "expected a break, got: {out}");
15422    }
15423
15424    #[test]
15425    fn go_loop_label_skipped_for_optional_match_but_kept_for_switch_match() {
15426        // An Optional match (`Some`/`None`) lowers to if/else: bare break ⇒ no
15427        // label needed.
15428        let opt_break = node(
15429            1,
15430            NodeKind::Match {
15431                scrutinee: Box::new(id_node(2, "o")),
15432                arms: vec![
15433                    node(
15434                        3,
15435                        NodeKind::MatchArm {
15436                            pattern: Box::new(node(
15437                                4,
15438                                NodeKind::ConstructorPat {
15439                                    path: type_path(&["Some"]),
15440                                    fields: vec![bind_pat(5, "x")],
15441                                },
15442                            )),
15443                            guard: None,
15444                            body: Box::new(block(6, vec![], Some(id_node(7, "x")))),
15445                        },
15446                    ),
15447                    node(
15448                        8,
15449                        NodeKind::MatchArm {
15450                            pattern: Box::new(node(
15451                                9,
15452                                NodeKind::ConstructorPat {
15453                                    path: type_path(&["None"]),
15454                                    fields: vec![],
15455                                },
15456                            )),
15457                            guard: None,
15458                            body: Box::new(block(
15459                                10,
15460                                vec![node(11, NodeKind::Break { value: None })],
15461                                None,
15462                            )),
15463                        },
15464                    ),
15465                ],
15466            },
15467        );
15468        assert!(
15469            !go_loop_needs_label(&opt_break),
15470            "Optional match-in-loop should not need a label"
15471        );
15472        // A non-Optional value-switch match with a `break` arm DOES need a label
15473        // (bare break would exit the Go switch, not the loop).
15474        let switch_break = node(
15475            20,
15476            NodeKind::Match {
15477                scrutinee: Box::new(id_node(21, "i")),
15478                arms: vec![
15479                    node(
15480                        22,
15481                        NodeKind::MatchArm {
15482                            pattern: Box::new(node(
15483                                23,
15484                                NodeKind::LiteralPat {
15485                                    lit: Literal::Int("5".into()),
15486                                },
15487                            )),
15488                            guard: None,
15489                            body: Box::new(block(
15490                                24,
15491                                vec![node(25, NodeKind::Break { value: None })],
15492                                None,
15493                            )),
15494                        },
15495                    ),
15496                    node(
15497                        26,
15498                        NodeKind::MatchArm {
15499                            pattern: Box::new(node(27, NodeKind::WildcardPat)),
15500                            guard: None,
15501                            body: Box::new(block(28, vec![], None)),
15502                        },
15503                    ),
15504                ],
15505            },
15506        );
15507        assert!(
15508            go_loop_needs_label(&switch_break),
15509            "non-Optional switch match with break should need a label"
15510        );
15511    }
15512
15513    #[test]
15514    fn async_main_no_wrapper() {
15515        // main is Go's entry — skip the wrapper to avoid dead code.
15516        let body = block(2, vec![], None);
15517        let f = node(
15518            1,
15519            NodeKind::FnDecl {
15520                annotations: vec![],
15521                visibility: Visibility::Private,
15522                is_async: true,
15523                name: ident("main"),
15524                generic_params: vec![],
15525                params: vec![],
15526                return_type: None,
15527                effect_clause: vec![],
15528                where_clause: vec![],
15529                body: Box::new(body),
15530            },
15531        );
15532        let out = gen(&module(vec![], vec![f]));
15533        assert!(out.contains("func main() {"), "got: {out}");
15534        assert!(!out.contains("mainAsync"), "got: {out}");
15535    }
15536
15537    #[test]
15538    fn async_call_rewritten_to_async_wrapper() {
15539        // Calling `task1()` from another async fn should route through
15540        // `Task1Async()` so callers can `await` (= `<-`) the channel.
15541        let task1 = node(
15542            10,
15543            NodeKind::FnDecl {
15544                annotations: vec![],
15545                visibility: Visibility::Public,
15546                is_async: true,
15547                name: ident("task1"),
15548                generic_params: vec![],
15549                params: vec![],
15550                return_type: Some(Box::new(node(
15551                    11,
15552                    NodeKind::TypeNamed {
15553                        path: type_path(&["Int"]),
15554                        args: vec![],
15555                    },
15556                ))),
15557                effect_clause: vec![],
15558                where_clause: vec![],
15559                body: Box::new(block(12, vec![], Some(int_lit(13, "1")))),
15560            },
15561        );
15562        // caller body: let a = task1(); let b = task1(); await a; await b
15563        let call_task1 = |id: u32| {
15564            node(
15565                id,
15566                NodeKind::Call {
15567                    callee: Box::new(id_node(id + 1, "task1")),
15568                    args: vec![],
15569                    type_args: vec![],
15570                },
15571            )
15572        };
15573        let let_stmt = |id: u32, name: &str, val: AIRNode| {
15574            node(
15575                id,
15576                NodeKind::LetBinding {
15577                    is_mut: false,
15578                    pattern: Box::new(bind_pat(id + 1, name)),
15579                    ty: None,
15580                    value: Box::new(val),
15581                },
15582            )
15583        };
15584        let await_id = |id: u32, name: &str| {
15585            node(
15586                id,
15587                NodeKind::Await {
15588                    expr: Box::new(id_node(id + 1, name)),
15589                },
15590            )
15591        };
15592        let caller_body = block(
15593            20,
15594            vec![
15595                let_stmt(30, "a", call_task1(31)),
15596                let_stmt(40, "b", call_task1(41)),
15597                let_stmt(50, "ra", await_id(51, "a")),
15598                let_stmt(60, "rb", await_id(61, "b")),
15599            ],
15600            None,
15601        );
15602        let caller = node(
15603            100,
15604            NodeKind::FnDecl {
15605                annotations: vec![],
15606                visibility: Visibility::Private,
15607                is_async: true,
15608                name: ident("run"),
15609                generic_params: vec![],
15610                params: vec![],
15611                return_type: None,
15612                effect_clause: vec![],
15613                where_clause: vec![],
15614                body: Box::new(caller_body),
15615            },
15616        );
15617        let out = gen(&module(vec![], vec![task1, caller]));
15618        // Concurrent goroutines: both bindings start channels.
15619        assert!(out.contains("a := Task1Async()"), "got: {out}");
15620        assert!(out.contains("b := Task1Async()"), "got: {out}");
15621        // Awaits receive from the channels.
15622        assert!(out.contains("ra := <-a"), "got: {out}");
15623        assert!(out.contains("rb := <-b"), "got: {out}");
15624    }
15625
15626    #[test]
15627    fn break_continue() {
15628        let brk = node(1, NodeKind::Break { value: None });
15629        let cont = node(2, NodeKind::Continue);
15630        let out = gen(&module(vec![], vec![brk, cont]));
15631        assert!(out.contains("break"), "got: {out}");
15632        assert!(out.contains("continue"), "got: {out}");
15633    }
15634
15635    #[test]
15636    fn guard_statement() {
15637        let g = node(
15638            1,
15639            NodeKind::Guard {
15640                let_pattern: None,
15641                condition: Box::new(bool_lit(2, true)),
15642                else_block: Box::new(block(
15643                    3,
15644                    vec![node(4, NodeKind::Return { value: None })],
15645                    None,
15646                )),
15647            },
15648        );
15649        let out = gen(&module(vec![], vec![g]));
15650        assert!(out.contains("if !(true)"), "got: {out}");
15651    }
15652
15653    #[test]
15654    fn ownership_erased() {
15655        let borrow = node(
15656            1,
15657            NodeKind::Borrow {
15658                expr: Box::new(id_node(2, "x")),
15659            },
15660        );
15661        let mv = node(
15662            3,
15663            NodeKind::Move {
15664                expr: Box::new(id_node(4, "y")),
15665            },
15666        );
15667        let out = gen(&module(vec![], vec![borrow, mv]));
15668        assert!(out.contains("x"), "got: {out}");
15669        assert!(out.contains("y"), "got: {out}");
15670        // Should NOT contain borrow/move keywords.
15671        assert!(!out.contains("&x"), "got: {out}");
15672    }
15673
15674    #[test]
15675    fn type_mapping() {
15676        let ctx = GoEmitCtx::new();
15677        assert_eq!(ctx.map_type_name("Int"), "int64");
15678        assert_eq!(ctx.map_type_name("Float"), "float64");
15679        assert_eq!(ctx.map_type_name("Bool"), "bool");
15680        assert_eq!(ctx.map_type_name("String"), "string");
15681        assert_eq!(ctx.map_type_name("Char"), "rune");
15682        assert_eq!(ctx.map_type_name("Void"), "struct{}");
15683        assert_eq!(ctx.map_type_name("Any"), "interface{}");
15684    }
15685
15686    #[test]
15687    fn parse_tuple_struct_field_types_round_trips_type_to_go() {
15688        // Inverse of `type_to_go`'s `TypeTuple` arm: parse the per-field Go types
15689        // back out of the rendered `struct{ Field0 T0; Field1 T1 }`.
15690        assert_eq!(
15691            GoEmitCtx::parse_tuple_struct_field_types("struct{ Field0 int64; Field1 int64 }"),
15692            vec!["int64".to_string(), "int64".to_string()]
15693        );
15694        assert_eq!(
15695            GoEmitCtx::parse_tuple_struct_field_types("struct{ Field0 int64; Field1 string }"),
15696            vec!["int64".to_string(), "string".to_string()]
15697        );
15698        // A non-tuple-struct string yields no fields (callers fall back to
15699        // element inference).
15700        assert!(GoEmitCtx::parse_tuple_struct_field_types("[]int64").is_empty());
15701        assert!(GoEmitCtx::parse_tuple_struct_field_types("int64").is_empty());
15702    }
15703
15704    /// Build a `TypeNamed { path: [name], args }` AIR node.
15705    fn type_named(name: &str, args: Vec<AIRNode>) -> AIRNode {
15706        node(
15707            900,
15708            NodeKind::TypeNamed {
15709                path: type_path(&[name]),
15710                args,
15711            },
15712        )
15713    }
15714
15715    /// The three collection types emit a concrete Go container with their
15716    /// element/key/value types recovered recursively, NOT the erased
15717    /// `interface{}` element (P3-α item 1a).
15718    #[test]
15719    fn type_to_go_collections_carry_element_types() {
15720        let ctx = GoEmitCtx::new();
15721        let int_ty = || type_named("Int", vec![]);
15722        let str_ty = || type_named("String", vec![]);
15723
15724        assert_eq!(
15725            ctx.type_to_go(&type_named("List", vec![int_ty()])),
15726            "[]int64"
15727        );
15728        assert_eq!(
15729            ctx.type_to_go(&type_named("Set", vec![int_ty()])),
15730            "map[int64]struct{}"
15731        );
15732        assert_eq!(
15733            ctx.type_to_go(&type_named("Map", vec![str_ty(), int_ty()])),
15734            "map[string]int64"
15735        );
15736        // Recursive: a list of maps.
15737        let inner_map = type_named("Map", vec![str_ty(), int_ty()]);
15738        assert_eq!(
15739            ctx.type_to_go(&type_named("List", vec![inner_map])),
15740            "[]map[string]int64"
15741        );
15742        // A bare collection with no type arg keeps the erased element.
15743        assert_eq!(ctx.type_to_go(&type_named("List", vec![])), "[]interface{}");
15744    }
15745
15746    /// Lifting the collection element type must NOT disturb the genuine runtime
15747    /// structs `Optional`/`Result`, which still erase their payload to the
15748    /// tagged runtime struct (`__bockOption` / `__bockResult`) — the regression
15749    /// the P3-α item 1a change was warned against.
15750    #[test]
15751    fn type_to_go_runtime_structs_unchanged() {
15752        let ctx = GoEmitCtx::new();
15753        let int_ty = || type_named("Int", vec![]);
15754        let str_ty = || type_named("String", vec![]);
15755        assert_eq!(
15756            ctx.type_to_go(&type_named("Optional", vec![int_ty()])),
15757            "__bockOption"
15758        );
15759        assert_eq!(
15760            ctx.type_to_go(&type_named("Result", vec![int_ty(), str_ty()])),
15761            "__bockResult"
15762        );
15763    }
15764
15765    /// `optional_inner_type_node` / `result_inner_type_nodes` peel one container
15766    /// layer for the nested-pattern declared-type threading: an
15767    /// `Optional[Result[(Int, Int), String]]` peels to its `Result[…]`, which
15768    /// peels to the `(Int, Int)` tuple and the `String` err. Rendering the peeled
15769    /// tuple node reproduces the concrete struct the nested tuple payload is
15770    /// asserted to.
15771    #[test]
15772    fn peel_optional_result_tuple_decl_type_nodes() {
15773        let ctx = GoEmitCtx::new();
15774        let int_ty = || type_named("Int", vec![]);
15775        let str_ty = || type_named("String", vec![]);
15776        let tuple_ty = node(
15777            910,
15778            NodeKind::TypeTuple {
15779                elems: vec![int_ty(), int_ty()],
15780            },
15781        );
15782        let result_ty = type_named("Result", vec![tuple_ty, str_ty()]);
15783        let opt_ty = type_named("Optional", vec![result_ty]);
15784
15785        // Optional → Result.
15786        let inner = ctx
15787            .optional_inner_type_node(&opt_ty)
15788            .expect("peels Optional");
15789        assert!(matches!(inner.kind, NodeKind::TypeNamed { .. }));
15790        // Result → (tuple ok, string err).
15791        let (ok, err) = ctx.result_inner_type_nodes(inner).expect("peels Result");
15792        assert_eq!(
15793            ctx.type_to_go(ok),
15794            "struct{ Field0 int64; Field1 int64 }",
15795            "the Ok payload is the concrete tuple struct"
15796        );
15797        assert_eq!(ctx.type_to_go(err.expect("err arg present")), "string");
15798        // A non-container type peels to nothing.
15799        assert!(ctx.optional_inner_type_node(&int_ty()).is_none());
15800        assert!(ctx.result_inner_type_nodes(&int_ty()).is_none());
15801    }
15802
15803    /// `peel_constructor_decl_ty` maps a tag to the inner declared type it carries
15804    /// (`Some`/`Ok` → ok/elem, `Err` → err), and `tuple_field_decl_tys` splits a
15805    /// declared tuple type into per-field nodes (or `None` on arity mismatch).
15806    #[test]
15807    fn constructor_and_tuple_decl_type_peeling() {
15808        let ctx = GoEmitCtx::new();
15809        let int_ty = || type_named("Int", vec![]);
15810        let str_ty = || type_named("String", vec![]);
15811        let result_ty = type_named("Result", vec![int_ty(), str_ty()]);
15812        let opt_ty = type_named("Optional", vec![result_ty.clone()]);
15813
15814        // `Some` peels Optional → Result (a runtime container, renders __bockResult).
15815        let some_inner = ctx
15816            .peel_constructor_decl_ty("Some", Some(&opt_ty))
15817            .expect("Some peels Optional");
15818        assert_eq!(ctx.type_to_go(&some_inner), "__bockResult");
15819        // `Ok` peels Result → Int; `Err` → String.
15820        let ok_inner = ctx
15821            .peel_constructor_decl_ty("Ok", Some(&result_ty))
15822            .expect("Ok peels Result");
15823        assert_eq!(ctx.type_to_go(&ok_inner), "int64");
15824        let err_inner = ctx
15825            .peel_constructor_decl_ty("Err", Some(&result_ty))
15826            .expect("Err peels Result");
15827        assert_eq!(ctx.type_to_go(&err_inner), "string");
15828        // Unknown declared type ⇒ no peel.
15829        assert!(ctx.peel_constructor_decl_ty("Some", None).is_none());
15830
15831        // A 2-tuple splits into two field nodes; an arity mismatch yields Nones.
15832        let tuple_ty = node(
15833            911,
15834            NodeKind::TypeTuple {
15835                elems: vec![int_ty(), str_ty()],
15836            },
15837        );
15838        let fields = ctx.tuple_field_decl_tys(Some(&tuple_ty), 2);
15839        assert_eq!(fields.len(), 2);
15840        assert_eq!(ctx.type_to_go(fields[0].expect("field 0")), "int64");
15841        assert_eq!(ctx.type_to_go(fields[1].expect("field 1")), "string");
15842        // Arity mismatch / unknown ⇒ all None.
15843        assert!(ctx
15844            .tuple_field_decl_tys(Some(&tuple_ty), 3)
15845            .iter()
15846            .all(Option::is_none));
15847        assert!(ctx
15848            .tuple_field_decl_tys(None, 2)
15849            .iter()
15850            .all(Option::is_none));
15851    }
15852
15853    /// `fn_type_go_signature` renders a declared `Fn(Int) -> Int` to its Go
15854    /// param/return types, used to type a lambda returned in tail position;
15855    /// `fn_type_ret_node` only keeps a function-typed return.
15856    #[test]
15857    fn fn_type_signature_for_returned_lambda() {
15858        let ctx = GoEmitCtx::new();
15859        let int_ty = || type_named("Int", vec![]);
15860        let fn_ty = node(
15861            912,
15862            NodeKind::TypeFunction {
15863                params: vec![int_ty()],
15864                ret: Box::new(int_ty()),
15865                effects: Vec::new(),
15866            },
15867        );
15868        let (params, ret) = ctx.fn_type_go_signature(&fn_ty).expect("is a fn type");
15869        assert_eq!(params, vec!["int64".to_string()]);
15870        assert_eq!(ret, "int64");
15871        // A non-function return type yields no signature / no kept node.
15872        assert!(ctx.fn_type_go_signature(&int_ty()).is_none());
15873        assert!(GoEmitCtx::fn_type_ret_node(Some(&int_ty())).is_none());
15874        assert!(GoEmitCtx::fn_type_ret_node(Some(&fn_ty)).is_some());
15875    }
15876
15877    /// `payload_access_go` asserts an Optional/Result *leaf* payload bind to its
15878    /// concrete element type — numeric via the widening helpers, others via a
15879    /// direct assertion — and reads the raw payload for unit/unknown types (a hard
15880    /// assertion on a boxed `nil` would panic).
15881    #[test]
15882    fn payload_access_typed_leaf_bind() {
15883        let ctx = GoEmitCtx::new();
15884        assert_eq!(
15885            ctx.payload_access_go("opt", Some("int64")),
15886            "__bockAsInt64(opt.v)"
15887        );
15888        assert_eq!(
15889            ctx.payload_access_go("opt", Some("float64")),
15890            "__bockAsFloat64(opt.v)"
15891        );
15892        assert_eq!(
15893            ctx.payload_access_go("res", Some("string")),
15894            "res.v.(string)"
15895        );
15896        assert_eq!(ctx.payload_access_go("opt", Some("struct{}")), "opt.v");
15897        assert_eq!(ctx.payload_access_go("opt", None), "opt.v");
15898    }
15899
15900    /// `specialise_lambda_param_types` sees through a `type` alias to a function
15901    /// type so a lambda argument bound to a `Predicate = Fn(Int) -> Bool`
15902    /// parameter is typed `func(x int64) bool`, not the erased `interface{}`.
15903    #[test]
15904    fn lambda_param_types_see_through_fn_type_alias() {
15905        let mut ctx = GoEmitCtx::new();
15906        let int_ty = || type_named("Int", vec![]);
15907        let bool_ty = || type_named("Bool", vec![]);
15908        let fn_ty = node(
15909            913,
15910            NodeKind::TypeFunction {
15911                params: vec![int_ty()],
15912                ret: Box::new(bool_ty()),
15913                effects: Vec::new(),
15914            },
15915        );
15916        // Register `type Predicate = Fn(Int) -> Bool`.
15917        ctx.type_aliases.insert("Predicate".to_string(), fn_ty);
15918        let alias = type_named("Predicate", vec![]);
15919        let tys = ctx
15920            .specialise_lambda_param_types(&alias, &[], &HashMap::new())
15921            .expect("alias resolves to a fn type");
15922        assert_eq!(tys, vec!["int64".to_string()]);
15923    }
15924
15925    #[test]
15926    fn naming_conventions() {
15927        assert_eq!(to_camel_case("hello_world"), "helloWorld");
15928        assert_eq!(to_camel_case("HelloWorld"), "helloWorld");
15929        assert_eq!(to_camel_case("already"), "already");
15930        assert_eq!(to_pascal_case("hello_world"), "HelloWorld");
15931        assert_eq!(to_pascal_case("helloWorld"), "HelloWorld");
15932        assert_eq!(to_pascal_case("Already"), "Already");
15933    }
15934
15935    #[test]
15936    fn escape_go_string_special_chars() {
15937        assert_eq!(escape_go_string("hello\nworld"), "hello\\nworld");
15938        assert_eq!(escape_go_string("tab\there"), "tab\\there");
15939        assert_eq!(escape_go_string("quote\"here"), "quote\\\"here");
15940    }
15941
15942    // ── End-to-end: syntax validation ───────────────────────────────────────
15943
15944    #[test]
15945    #[ignore] // requires `go` to be installed
15946    fn generated_go_passes_vet() {
15947        let body = block(
15948            2,
15949            vec![],
15950            Some(node(
15951                3,
15952                NodeKind::Interpolation {
15953                    parts: vec![
15954                        AirInterpolationPart::Literal("Hello, ".into()),
15955                        AirInterpolationPart::Expr(Box::new(id_node(4, "name"))),
15956                    ],
15957                },
15958            )),
15959        );
15960        let f = node(
15961            1,
15962            NodeKind::FnDecl {
15963                annotations: vec![],
15964                visibility: Visibility::Public,
15965                is_async: false,
15966                name: ident("greet"),
15967                generic_params: vec![],
15968                params: vec![typed_param_node(5, "name", "String")],
15969                return_type: Some(Box::new(node(
15970                    6,
15971                    NodeKind::TypeNamed {
15972                        path: type_path(&["String"]),
15973                        args: vec![],
15974                    },
15975                ))),
15976                effect_clause: vec![],
15977                where_clause: vec![],
15978                body: Box::new(body),
15979            },
15980        );
15981        let code = gen(&module(vec![], vec![f]));
15982
15983        // Write to temp file and run go vet.
15984        let dir = std::env::temp_dir().join("bock_go_test");
15985        let _ = std::fs::create_dir_all(&dir);
15986        let file_path = dir.join("output.go");
15987        std::fs::write(&file_path, &code).unwrap();
15988
15989        let output = std::process::Command::new("go")
15990            .args(["vet", file_path.to_str().unwrap()])
15991            .output();
15992        match output {
15993            Ok(o) => {
15994                if !o.status.success() {
15995                    let stderr = String::from_utf8_lossy(&o.stderr);
15996                    panic!("go vet failed:\n{stderr}\n\nGenerated code:\n{code}");
15997                }
15998            }
15999            Err(e) => {
16000                panic!("Failed to run go vet: {e}");
16001            }
16002        }
16003        let _ = std::fs::remove_dir_all(&dir);
16004    }
16005
16006    #[test]
16007    #[ignore] // requires `go` to be installed
16008    fn generated_go_compiles_and_runs() {
16009        // Build a complete Go program that prints "42".
16010        let body = block(
16011            2,
16012            vec![node(
16013                3,
16014                NodeKind::LetBinding {
16015                    is_mut: false,
16016                    pattern: Box::new(bind_pat(4, "x")),
16017                    ty: None,
16018                    value: Box::new(int_lit(5, "42")),
16019                },
16020            )],
16021            None,
16022        );
16023        let main_fn = node(
16024            1,
16025            NodeKind::FnDecl {
16026                annotations: vec![],
16027                visibility: Visibility::Private,
16028                is_async: false,
16029                name: ident("main"),
16030                generic_params: vec![],
16031                params: vec![],
16032                return_type: None,
16033                effect_clause: vec![],
16034                where_clause: vec![],
16035                body: Box::new(body),
16036            },
16037        );
16038        let code = gen(&module(vec![], vec![main_fn]));
16039
16040        let dir = std::env::temp_dir().join("bock_go_run_test");
16041        let _ = std::fs::create_dir_all(&dir);
16042        let file_path = dir.join("main.go");
16043        std::fs::write(&file_path, &code).unwrap();
16044
16045        let output = std::process::Command::new("go")
16046            .args(["build", file_path.to_str().unwrap()])
16047            .current_dir(&dir)
16048            .output();
16049        match output {
16050            Ok(o) => {
16051                if !o.status.success() {
16052                    let stderr = String::from_utf8_lossy(&o.stderr);
16053                    panic!("go build failed:\n{stderr}\n\nGenerated code:\n{code}");
16054                }
16055            }
16056            Err(e) => {
16057                panic!("Failed to run go build: {e}");
16058            }
16059        }
16060        let _ = std::fs::remove_dir_all(&dir);
16061    }
16062
16063    #[test]
16064    fn expr_match_no_unused_var() {
16065        // Expression-position match should not emit unused `__v`.
16066        let match_expr = node(
16067            1,
16068            NodeKind::Match {
16069                scrutinee: Box::new(id_node(2, "x")),
16070                arms: vec![
16071                    node(
16072                        3,
16073                        NodeKind::MatchArm {
16074                            pattern: Box::new(node(
16075                                4,
16076                                NodeKind::LiteralPat {
16077                                    lit: Literal::Int("1".into()),
16078                                },
16079                            )),
16080                            guard: None,
16081                            body: Box::new(block(5, vec![], Some(str_lit(6, "one")))),
16082                        },
16083                    ),
16084                    node(
16085                        7,
16086                        NodeKind::MatchArm {
16087                            pattern: Box::new(node(8, NodeKind::WildcardPat)),
16088                            guard: None,
16089                            body: Box::new(block(9, vec![], Some(str_lit(10, "other")))),
16090                        },
16091                    ),
16092                ],
16093            },
16094        );
16095        // Emit in expression context via a let binding.
16096        let let_node = node(
16097            20,
16098            NodeKind::LetBinding {
16099                is_mut: false,
16100                pattern: Box::new(bind_pat(21, "result")),
16101                ty: None,
16102                value: Box::new(match_expr),
16103            },
16104        );
16105        let out = gen(&module(vec![], vec![let_node]));
16106        assert!(
16107            !out.contains("__v"),
16108            "expression-position match should not emit __v, got: {out}"
16109        );
16110        assert!(
16111            out.contains("switch x"),
16112            "should emit switch with scrutinee directly, got: {out}"
16113        );
16114    }
16115
16116    // ── Prelude function mapping tests ──────────────────────────────────────
16117
16118    /// Helper: generate Go for a module with a `main` function containing a single call.
16119    fn gen_prelude_call(func_name: &str, arg: AIRNode) -> String {
16120        let call = node(
16121            10,
16122            NodeKind::Call {
16123                callee: Box::new(id_node(11, func_name)),
16124                args: vec![AirArg {
16125                    label: None,
16126                    value: arg,
16127                }],
16128                type_args: vec![],
16129            },
16130        );
16131        let body = block(2, vec![call], None);
16132        let f = node(
16133            1,
16134            NodeKind::FnDecl {
16135                name: ident("main"),
16136                params: vec![],
16137                return_type: None,
16138                body: Box::new(body),
16139                generic_params: vec![],
16140                visibility: Visibility::Private,
16141                annotations: vec![],
16142                effect_clause: vec![],
16143                where_clause: vec![],
16144                is_async: false,
16145            },
16146        );
16147        gen(&module(vec![], vec![f]))
16148    }
16149
16150    /// Helper: generate Go for a nullary prelude call (no args).
16151    fn gen_prelude_call_no_args(func_name: &str) -> String {
16152        let call = node(
16153            10,
16154            NodeKind::Call {
16155                callee: Box::new(id_node(11, func_name)),
16156                args: vec![],
16157                type_args: vec![],
16158            },
16159        );
16160        let body = block(2, vec![call], None);
16161        let f = node(
16162            1,
16163            NodeKind::FnDecl {
16164                name: ident("main"),
16165                params: vec![],
16166                return_type: None,
16167                body: Box::new(body),
16168                generic_params: vec![],
16169                visibility: Visibility::Private,
16170                annotations: vec![],
16171                effect_clause: vec![],
16172                where_clause: vec![],
16173                is_async: false,
16174            },
16175        );
16176        gen(&module(vec![], vec![f]))
16177    }
16178
16179    #[test]
16180    fn prelude_println_maps_to_fmt_println() {
16181        let out = gen_prelude_call("println", str_lit(12, "hello"));
16182        assert!(
16183            out.contains("fmt.Println("),
16184            "println should map to fmt.Println, got: {out}"
16185        );
16186        assert!(
16187            !out.contains("println("),
16188            "should not emit bare println(, got: {out}"
16189        );
16190    }
16191
16192    #[test]
16193    fn prelude_print_maps_to_fmt_print() {
16194        let out = gen_prelude_call("print", str_lit(12, "hello"));
16195        assert!(
16196            out.contains("fmt.Print("),
16197            "print should map to fmt.Print, got: {out}"
16198        );
16199    }
16200
16201    #[test]
16202    fn prelude_debug_maps_to_fmt_printf() {
16203        let out = gen_prelude_call("debug", str_lit(12, "val"));
16204        assert!(
16205            out.contains("fmt.Printf(\"%+v\\n\", "),
16206            "debug should map to fmt.Printf, got: {out}"
16207        );
16208    }
16209
16210    #[test]
16211    fn prelude_assert_maps_to_panic() {
16212        let out = gen_prelude_call("assert", bool_lit(12, true));
16213        assert!(
16214            out.contains("if !true { panic(\"assertion failed\") }"),
16215            "assert should map to if-panic, got: {out}"
16216        );
16217    }
16218
16219    #[test]
16220    fn prelude_todo_maps_to_panic_not_implemented() {
16221        let out = gen_prelude_call_no_args("todo");
16222        assert!(
16223            out.contains("panic(\"not implemented\")"),
16224            "todo should map to panic, got: {out}"
16225        );
16226    }
16227
16228    #[test]
16229    fn prelude_unreachable_maps_to_panic_unreachable() {
16230        let out = gen_prelude_call_no_args("unreachable");
16231        assert!(
16232            out.contains("panic(\"unreachable\")"),
16233            "unreachable should map to panic, got: {out}"
16234        );
16235    }
16236
16237    #[test]
16238    fn non_prelude_call_passes_through() {
16239        let out = gen_prelude_call("my_custom_func", str_lit(12, "arg"));
16240        assert!(
16241            out.contains("myCustomFunc("),
16242            "non-prelude call should use camelCase, got: {out}"
16243        );
16244    }
16245
16246    #[test]
16247    fn handling_block_passes_handlers_to_effectful_call() {
16248        use bock_air::AirHandlerPair;
16249
16250        let effect_decl = node(
16251            1,
16252            NodeKind::EffectDecl {
16253                annotations: vec![],
16254                visibility: Visibility::Public,
16255                name: ident("Logger"),
16256                generic_params: vec![],
16257                components: vec![],
16258                operations: vec![node(
16259                    2,
16260                    NodeKind::FnDecl {
16261                        annotations: vec![],
16262                        visibility: Visibility::Public,
16263                        is_async: false,
16264                        name: ident("log"),
16265                        generic_params: vec![],
16266                        params: vec![typed_param_node(3, "msg", "String")],
16267                        return_type: None,
16268                        effect_clause: vec![],
16269                        where_clause: vec![],
16270                        body: Box::new(block(4, vec![], None)),
16271                    },
16272                )],
16273            },
16274        );
16275
16276        let inner_fn = node(
16277            10,
16278            NodeKind::FnDecl {
16279                annotations: vec![],
16280                visibility: Visibility::Private,
16281                is_async: false,
16282                name: ident("inner"),
16283                generic_params: vec![],
16284                params: vec![],
16285                return_type: None,
16286                effect_clause: vec![type_path(&["Logger"])],
16287                where_clause: vec![],
16288                body: Box::new(block(12, vec![], Some(str_lit(13, "hello")))),
16289            },
16290        );
16291
16292        let call_inner = node(
16293            20,
16294            NodeKind::Call {
16295                callee: Box::new(id_node(21, "inner")),
16296                args: vec![],
16297                type_args: vec![],
16298            },
16299        );
16300        let handling = node(
16301            30,
16302            NodeKind::HandlingBlock {
16303                handlers: vec![AirHandlerPair {
16304                    effect: type_path(&["Logger"]),
16305                    handler: Box::new(node(
16306                        31,
16307                        NodeKind::Call {
16308                            callee: Box::new(id_node(32, "StdoutLogger")),
16309                            args: vec![],
16310                            type_args: vec![],
16311                        },
16312                    )),
16313                }],
16314                body: Box::new(block(33, vec![], Some(call_inner))),
16315            },
16316        );
16317        let main_fn = node(
16318            40,
16319            NodeKind::FnDecl {
16320                annotations: vec![],
16321                visibility: Visibility::Private,
16322                is_async: false,
16323                name: ident("main"),
16324                generic_params: vec![],
16325                params: vec![],
16326                return_type: None,
16327                effect_clause: vec![],
16328                where_clause: vec![],
16329                body: Box::new(block(41, vec![handling], None)),
16330            },
16331        );
16332
16333        let out = gen(&module(vec![], vec![effect_decl, inner_fn, main_fn]));
16334        // Go: inner(__logger)
16335        assert!(
16336            out.contains("inner(__logger)"),
16337            "handling block should pass handler to effectful call, got: {out}"
16338        );
16339        assert!(
16340            out.contains("__logger := stdoutLogger()"),
16341            "handling block should instantiate handler, got: {out}"
16342        );
16343    }
16344
16345    #[test]
16346    fn sibling_handling_blocks_do_not_share_go_block_scope() {
16347        use bock_air::AirHandlerPair;
16348
16349        // Two *sibling* `handling` blocks, each `let part = …` under the SAME
16350        // name. Each block lowers to its own `{ … }` Go block scope, so both
16351        // must declare a fresh `part := …` — neither may be rewritten into a
16352        // bare `part = …` assignment against the other (a name that left scope
16353        // when the first block closed; Go would reject it as `undefined: part`).
16354        // Regression for Q-go-handling-let-redeclaration (mirrors the js/ts fix
16355        // Q-js-handling-let-redeclaration, #371).
16356        let make_handling = |id: u32, val: &str| {
16357            node(
16358                id,
16359                NodeKind::HandlingBlock {
16360                    handlers: vec![AirHandlerPair {
16361                        effect: type_path(&["Logger"]),
16362                        handler: Box::new(node(
16363                            id + 1,
16364                            NodeKind::Call {
16365                                callee: Box::new(id_node(id + 2, "StdoutLogger")),
16366                                args: vec![],
16367                                type_args: vec![],
16368                            },
16369                        )),
16370                    }],
16371                    body: Box::new(block(
16372                        id + 3,
16373                        vec![node(
16374                            id + 4,
16375                            NodeKind::LetBinding {
16376                                is_mut: false,
16377                                pattern: Box::new(bind_pat(id + 5, "part")),
16378                                ty: None,
16379                                value: Box::new(str_lit(id + 6, val)),
16380                            },
16381                        )],
16382                        Some(id_node(id + 7, "part")),
16383                    )),
16384                },
16385            )
16386        };
16387        let main_fn = node(
16388            40,
16389            NodeKind::FnDecl {
16390                annotations: vec![],
16391                visibility: Visibility::Private,
16392                is_async: false,
16393                name: ident("main"),
16394                generic_params: vec![],
16395                params: vec![],
16396                return_type: None,
16397                effect_clause: vec![],
16398                where_clause: vec![],
16399                body: Box::new(block(
16400                    41,
16401                    vec![make_handling(50, "first"), make_handling(70, "second")],
16402                    None,
16403                )),
16404            },
16405        );
16406
16407        let out = gen(&module(vec![], vec![main_fn]));
16408        assert_eq!(
16409            out.matches("part := ").count(),
16410            2,
16411            "each sibling handling block should declare its own `part := …`, got: {out}"
16412        );
16413        assert!(
16414            !out.contains("part = \""),
16415            "no sibling handling block may rewrite its `let part` into a bare \
16416             assignment, got: {out}"
16417        );
16418    }
16419
16420    // ── C.8 Go effect codegen polish tests ──────────────────────────────────
16421
16422    fn type_named_node(id: u32, name: &str) -> AIRNode {
16423        node(
16424            id,
16425            NodeKind::TypeNamed {
16426                path: type_path(&[name]),
16427                args: vec![],
16428            },
16429        )
16430    }
16431
16432    /// Effect interface: Void-returning operations emit no return type.
16433    #[test]
16434    fn effect_interface_drops_void_return_type() {
16435        let void_op = node(
16436            2,
16437            NodeKind::FnDecl {
16438                annotations: vec![],
16439                visibility: Visibility::Public,
16440                is_async: false,
16441                name: ident("log"),
16442                generic_params: vec![],
16443                params: vec![typed_param_node(3, "msg", "String")],
16444                return_type: Some(Box::new(type_named_node(4, "Void"))),
16445                effect_clause: vec![],
16446                where_clause: vec![],
16447                body: Box::new(block(5, vec![], None)),
16448            },
16449        );
16450        let effect_decl = node(
16451            1,
16452            NodeKind::EffectDecl {
16453                annotations: vec![],
16454                visibility: Visibility::Public,
16455                name: ident("Logger"),
16456                generic_params: vec![],
16457                components: vec![],
16458                operations: vec![void_op],
16459            },
16460        );
16461        let out = gen(&module(vec![], vec![effect_decl]));
16462        assert!(
16463            out.contains("type Logger interface {"),
16464            "should emit interface, got: {out}"
16465        );
16466        assert!(
16467            out.contains("Log(string)\n"),
16468            "Void op should have no return type, got: {out}"
16469        );
16470        assert!(
16471            !out.contains("Log(string) struct{}"),
16472            "Void op should NOT emit struct{{}} return, got: {out}"
16473        );
16474    }
16475
16476    /// Public effectful function: Void return type is dropped in Go signature.
16477    #[test]
16478    fn fn_decl_drops_void_return_type() {
16479        let f = node(
16480            10,
16481            NodeKind::FnDecl {
16482                annotations: vec![],
16483                visibility: Visibility::Public,
16484                is_async: false,
16485                name: ident("do_thing"),
16486                generic_params: vec![],
16487                params: vec![],
16488                return_type: Some(Box::new(type_named_node(11, "Void"))),
16489                effect_clause: vec![],
16490                where_clause: vec![],
16491                body: Box::new(block(12, vec![], None)),
16492            },
16493        );
16494        let out = gen(&module(vec![], vec![f]));
16495        assert!(
16496            out.contains("func DoThing() {"),
16497            "Void fn should have no return type, got: {out}"
16498        );
16499        assert!(
16500            !out.contains("DoThing() struct{}"),
16501            "should not emit struct{{}} return, got: {out}"
16502        );
16503    }
16504
16505    /// Public function call sites emit PascalCase matching their definition.
16506    #[test]
16507    fn call_site_uses_pascal_case_for_public_fn() {
16508        let pub_fn = node(
16509            10,
16510            NodeKind::FnDecl {
16511                annotations: vec![],
16512                visibility: Visibility::Public,
16513                is_async: false,
16514                name: ident("do_thing"),
16515                generic_params: vec![],
16516                params: vec![],
16517                return_type: None,
16518                effect_clause: vec![],
16519                where_clause: vec![],
16520                body: Box::new(block(12, vec![], None)),
16521            },
16522        );
16523        let call = node(
16524            20,
16525            NodeKind::Call {
16526                callee: Box::new(id_node(21, "do_thing")),
16527                args: vec![],
16528                type_args: vec![],
16529            },
16530        );
16531        let main_fn = node(
16532            30,
16533            NodeKind::FnDecl {
16534                annotations: vec![],
16535                visibility: Visibility::Private,
16536                is_async: false,
16537                name: ident("main"),
16538                generic_params: vec![],
16539                params: vec![],
16540                return_type: None,
16541                effect_clause: vec![],
16542                where_clause: vec![],
16543                body: Box::new(block(31, vec![], Some(call))),
16544            },
16545        );
16546        let out = gen(&module(vec![], vec![pub_fn, main_fn]));
16547        assert!(
16548            out.contains("DoThing()"),
16549            "call to public fn should be PascalCase, got: {out}"
16550        );
16551        assert!(
16552            !out.contains("doThing()"),
16553            "call should NOT use camelCase for public fn, got: {out}"
16554        );
16555    }
16556
16557    /// Go forbids a struct having a field and a method with the same name. A
16558    /// record whose field name collides with a method's PascalCased Go name (the
16559    /// `core.error` shape: `record SimpleError { message: String }` +
16560    /// `fn message(self) -> String`) must emit the method under a disambiguated
16561    /// name (`MessageMethod`) at the *trait interface*, the *receiver method*,
16562    /// and every *call site* so they agree — while the field stays `Message`.
16563    /// Q-go-error-message: pre-S6b this emitted both `Message` field and
16564    /// `Message()` method on `SimpleError`, which `go build` rejects.
16565    #[test]
16566    fn method_colliding_with_field_is_disambiguated() {
16567        // record SimpleError { message: String }
16568        let record_decl = node(
16569            1,
16570            NodeKind::RecordDecl {
16571                annotations: vec![],
16572                visibility: Visibility::Public,
16573                name: ident("SimpleError"),
16574                generic_params: vec![],
16575                fields: vec![bock_ast::RecordDeclField {
16576                    id: 0,
16577                    span: span(),
16578                    name: ident("message"),
16579                    ty: TypeExpr::Named {
16580                        id: 0,
16581                        span: span(),
16582                        path: type_path(&["String"]),
16583                        args: vec![],
16584                    },
16585                    default: None,
16586                }],
16587            },
16588        );
16589        // trait Error { fn message(self) -> String }
16590        let trait_decl = node(
16591            2,
16592            NodeKind::TraitDecl {
16593                annotations: vec![],
16594                visibility: Visibility::Public,
16595                is_platform: false,
16596                name: ident("Error"),
16597                generic_params: vec![],
16598                associated_types: vec![],
16599                methods: vec![node(
16600                    3,
16601                    NodeKind::FnDecl {
16602                        annotations: vec![],
16603                        visibility: Visibility::Public,
16604                        is_async: false,
16605                        name: ident("message"),
16606                        generic_params: vec![],
16607                        params: vec![param_node(4, "self")],
16608                        return_type: Some(Box::new(type_named_node(5, "String"))),
16609                        effect_clause: vec![],
16610                        where_clause: vec![],
16611                        body: Box::new(block(6, vec![], None)),
16612                    },
16613                )],
16614            },
16615        );
16616        // impl Error for SimpleError { public fn message(self) -> String { self.message } }
16617        let method = node(
16618            10,
16619            NodeKind::FnDecl {
16620                annotations: vec![],
16621                visibility: Visibility::Public,
16622                is_async: false,
16623                name: ident("message"),
16624                generic_params: vec![],
16625                params: vec![param_node(11, "self")],
16626                return_type: Some(Box::new(type_named_node(12, "String"))),
16627                effect_clause: vec![],
16628                where_clause: vec![],
16629                body: Box::new(block(
16630                    13,
16631                    vec![],
16632                    Some(node(
16633                        14,
16634                        NodeKind::FieldAccess {
16635                            object: Box::new(id_node(15, "self")),
16636                            field: ident("message"),
16637                        },
16638                    )),
16639                )),
16640            },
16641        );
16642        let impl_block = node(
16643            20,
16644            NodeKind::ImplBlock {
16645                annotations: vec![],
16646                target: Box::new(type_named_node(21, "SimpleError")),
16647                trait_path: Some(type_path(&["Error"])),
16648                trait_args: vec![],
16649                generic_params: vec![],
16650                where_clause: vec![],
16651                methods: vec![method],
16652            },
16653        );
16654        // fn read(e: SimpleError) -> String { e.message() }
16655        let read_fn = node(
16656            30,
16657            NodeKind::FnDecl {
16658                annotations: vec![],
16659                visibility: Visibility::Public,
16660                is_async: false,
16661                name: ident("read"),
16662                generic_params: vec![],
16663                params: vec![typed_param_node(31, "e", "SimpleError")],
16664                return_type: Some(Box::new(type_named_node(32, "String"))),
16665                effect_clause: vec![],
16666                where_clause: vec![],
16667                body: Box::new(block(
16668                    33,
16669                    vec![],
16670                    Some(node(
16671                        34,
16672                        NodeKind::MethodCall {
16673                            receiver: Box::new(id_node(35, "e")),
16674                            method: ident("message"),
16675                            type_args: vec![],
16676                            args: vec![],
16677                        },
16678                    )),
16679                )),
16680            },
16681        );
16682        let out = gen(&module(
16683            vec![],
16684            vec![record_decl, trait_decl, impl_block, read_fn],
16685        ));
16686        // The field stays `Message`.
16687        assert!(
16688            out.contains("Message\tstring"),
16689            "field should remain `Message`, got: {out}"
16690        );
16691        // The method (interface, receiver, call site) is disambiguated.
16692        assert!(
16693            out.contains("MessageMethod() string"),
16694            "trait interface should declare `MessageMethod()`, got: {out}"
16695        );
16696        assert!(
16697            out.contains("SimpleError) MessageMethod()"),
16698            "receiver method should be `MessageMethod()`, got: {out}"
16699        );
16700        assert!(
16701            out.contains(".MessageMethod()"),
16702            "call site should be `.MessageMethod()`, got: {out}"
16703        );
16704        // The body still reads the field (`self.Message`), and no plain
16705        // `Message()` method (the colliding form Go rejects) is emitted.
16706        assert!(
16707            out.contains("return self.Message"),
16708            "method body should read the field `self.Message`, got: {out}"
16709        );
16710        assert!(
16711            !out.contains(") Message() string"),
16712            "must NOT emit a `Message()` method colliding with the field, got: {out}"
16713        );
16714    }
16715
16716    /// Trait/effect impl blocks use value receivers so `Handler{}` satisfies the interface.
16717    #[test]
16718    fn impl_block_methods_use_value_receivers() {
16719        let record_decl = node(
16720            1,
16721            NodeKind::RecordDecl {
16722                annotations: vec![],
16723                visibility: Visibility::Public,
16724                name: ident("StdoutLogger"),
16725                generic_params: vec![],
16726                fields: vec![],
16727            },
16728        );
16729        let method = node(
16730            10,
16731            NodeKind::FnDecl {
16732                annotations: vec![],
16733                visibility: Visibility::Public,
16734                is_async: false,
16735                name: ident("log"),
16736                generic_params: vec![],
16737                // Instance method leads with `self` (real lowering); a no-`self`
16738                // method is an associated function (free function, no receiver).
16739                params: vec![
16740                    param_node(14, "self"),
16741                    typed_param_node(11, "msg", "String"),
16742                ],
16743                return_type: Some(Box::new(type_named_node(12, "Void"))),
16744                effect_clause: vec![],
16745                where_clause: vec![],
16746                body: Box::new(block(13, vec![], None)),
16747            },
16748        );
16749        let impl_block = node(
16750            20,
16751            NodeKind::ImplBlock {
16752                annotations: vec![],
16753                target: Box::new(type_named_node(21, "StdoutLogger")),
16754                trait_path: Some(type_path(&["Logger"])),
16755                trait_args: vec![],
16756                generic_params: vec![],
16757                where_clause: vec![],
16758                methods: vec![method],
16759            },
16760        );
16761        let out = gen(&module(vec![], vec![record_decl, impl_block]));
16762        assert!(
16763            out.contains("func (self StdoutLogger) Log("),
16764            "impl method should use value receiver, got: {out}"
16765        );
16766        assert!(
16767            !out.contains("func (self *StdoutLogger) Log("),
16768            "impl method should NOT use pointer receiver, got: {out}"
16769        );
16770    }
16771
16772    /// Module-level `handle` declares a var AND registers it so module-level
16773    /// calls to effectful functions pick it up.
16774    #[test]
16775    fn module_handle_registers_handler_for_calls() {
16776        use bock_air::AirHandlerPair;
16777        let _ = AirHandlerPair {
16778            effect: type_path(&["Logger"]),
16779            handler: Box::new(str_lit(999, "placeholder")),
16780        };
16781
16782        let effect_decl = node(
16783            1,
16784            NodeKind::EffectDecl {
16785                annotations: vec![],
16786                visibility: Visibility::Public,
16787                name: ident("Logger"),
16788                generic_params: vec![],
16789                components: vec![],
16790                operations: vec![node(
16791                    2,
16792                    NodeKind::FnDecl {
16793                        annotations: vec![],
16794                        visibility: Visibility::Public,
16795                        is_async: false,
16796                        name: ident("log"),
16797                        generic_params: vec![],
16798                        params: vec![typed_param_node(3, "msg", "String")],
16799                        return_type: Some(Box::new(type_named_node(4, "Void"))),
16800                        effect_clause: vec![],
16801                        where_clause: vec![],
16802                        body: Box::new(block(5, vec![], None)),
16803                    },
16804                )],
16805            },
16806        );
16807
16808        let effectful_fn = node(
16809            10,
16810            NodeKind::FnDecl {
16811                annotations: vec![],
16812                visibility: Visibility::Public,
16813                is_async: false,
16814                name: ident("do_log"),
16815                generic_params: vec![],
16816                params: vec![],
16817                return_type: None,
16818                effect_clause: vec![type_path(&["Logger"])],
16819                where_clause: vec![],
16820                body: Box::new(block(11, vec![], None)),
16821            },
16822        );
16823
16824        let module_handle = node(
16825            20,
16826            NodeKind::ModuleHandle {
16827                effect: type_path(&["Logger"]),
16828                handler: Box::new(node(
16829                    21,
16830                    NodeKind::Call {
16831                        callee: Box::new(id_node(22, "StdoutLogger")),
16832                        args: vec![],
16833                        type_args: vec![],
16834                    },
16835                )),
16836            },
16837        );
16838
16839        let main_call = node(
16840            30,
16841            NodeKind::Call {
16842                callee: Box::new(id_node(31, "do_log")),
16843                args: vec![],
16844                type_args: vec![],
16845            },
16846        );
16847        let main_fn = node(
16848            40,
16849            NodeKind::FnDecl {
16850                annotations: vec![],
16851                visibility: Visibility::Private,
16852                is_async: false,
16853                name: ident("main"),
16854                generic_params: vec![],
16855                params: vec![],
16856                return_type: None,
16857                effect_clause: vec![],
16858                where_clause: vec![],
16859                body: Box::new(block(41, vec![], Some(main_call))),
16860            },
16861        );
16862
16863        let out = gen(&module(
16864            vec![],
16865            vec![effect_decl, effectful_fn, module_handle, main_fn],
16866        ));
16867        assert!(
16868            out.contains("var __logger Logger = stdoutLogger()"),
16869            "module handle should declare var, got: {out}"
16870        );
16871        assert!(
16872            out.contains("DoLog(__logger)"),
16873            "module-level call should receive __logger, got: {out}"
16874        );
16875    }
16876
16877    /// Handling block suppresses Go "declared but not used" errors for handler vars.
16878    #[test]
16879    fn handling_block_emits_unused_suppression() {
16880        use bock_air::AirHandlerPair;
16881        let effect_decl = node(
16882            1,
16883            NodeKind::EffectDecl {
16884                annotations: vec![],
16885                visibility: Visibility::Public,
16886                name: ident("Logger"),
16887                generic_params: vec![],
16888                components: vec![],
16889                operations: vec![node(
16890                    2,
16891                    NodeKind::FnDecl {
16892                        annotations: vec![],
16893                        visibility: Visibility::Public,
16894                        is_async: false,
16895                        name: ident("log"),
16896                        generic_params: vec![],
16897                        params: vec![typed_param_node(3, "msg", "String")],
16898                        return_type: Some(Box::new(type_named_node(4, "Void"))),
16899                        effect_clause: vec![],
16900                        where_clause: vec![],
16901                        body: Box::new(block(5, vec![], None)),
16902                    },
16903                )],
16904            },
16905        );
16906        let handling = node(
16907            30,
16908            NodeKind::HandlingBlock {
16909                handlers: vec![AirHandlerPair {
16910                    effect: type_path(&["Logger"]),
16911                    handler: Box::new(node(
16912                        31,
16913                        NodeKind::Call {
16914                            callee: Box::new(id_node(32, "StdoutLogger")),
16915                            args: vec![],
16916                            type_args: vec![],
16917                        },
16918                    )),
16919                }],
16920                body: Box::new(block(33, vec![], Some(str_lit(34, "body")))),
16921            },
16922        );
16923        let main_fn = node(
16924            40,
16925            NodeKind::FnDecl {
16926                annotations: vec![],
16927                visibility: Visibility::Private,
16928                is_async: false,
16929                name: ident("main"),
16930                generic_params: vec![],
16931                params: vec![],
16932                return_type: None,
16933                effect_clause: vec![],
16934                where_clause: vec![],
16935                body: Box::new(block(41, vec![handling], None)),
16936            },
16937        );
16938        let out = gen(&module(vec![], vec![effect_decl, main_fn]));
16939        assert!(
16940            out.contains("_ = __logger"),
16941            "should suppress unused-var error for handler, got: {out}"
16942        );
16943    }
16944
16945    /// Void effect operations (e.g., log) are not wrapped in `return` when a
16946    /// tail expression in a Void-returning function.
16947    #[test]
16948    fn void_effect_op_tail_not_wrapped_in_return() {
16949        let effect_decl = node(
16950            1,
16951            NodeKind::EffectDecl {
16952                annotations: vec![],
16953                visibility: Visibility::Public,
16954                name: ident("Logger"),
16955                generic_params: vec![],
16956                components: vec![],
16957                operations: vec![node(
16958                    2,
16959                    NodeKind::FnDecl {
16960                        annotations: vec![],
16961                        visibility: Visibility::Public,
16962                        is_async: false,
16963                        name: ident("log"),
16964                        generic_params: vec![],
16965                        params: vec![typed_param_node(3, "msg", "String")],
16966                        return_type: Some(Box::new(type_named_node(4, "Void"))),
16967                        effect_clause: vec![],
16968                        where_clause: vec![],
16969                        body: Box::new(block(5, vec![], None)),
16970                    },
16971                )],
16972            },
16973        );
16974        let log_call = node(
16975            10,
16976            NodeKind::Call {
16977                callee: Box::new(id_node(11, "log")),
16978                args: vec![bock_air::AirArg {
16979                    label: None,
16980                    value: str_lit(12, "hello"),
16981                }],
16982                type_args: vec![],
16983            },
16984        );
16985        let caller = node(
16986            20,
16987            NodeKind::FnDecl {
16988                annotations: vec![],
16989                visibility: Visibility::Public,
16990                is_async: false,
16991                name: ident("do_log"),
16992                generic_params: vec![],
16993                params: vec![],
16994                return_type: Some(Box::new(type_named_node(21, "Void"))),
16995                effect_clause: vec![type_path(&["Logger"])],
16996                where_clause: vec![],
16997                body: Box::new(block(22, vec![], Some(log_call))),
16998            },
16999        );
17000        let out = gen(&module(vec![], vec![effect_decl, caller]));
17001        assert!(
17002            out.contains("logger.Log("),
17003            "effect op should be rewritten as handler.Method, got: {out}"
17004        );
17005        assert!(
17006            !out.contains("return logger.Log("),
17007            "Void effect op in Void fn should NOT be preceded by `return`, got: {out}"
17008        );
17009    }
17010
17011    // ── Generics codegen (DV12 / P1-b2) ───────────────────────────────────────
17012
17013    fn generic_param(id: u32, name: &str) -> bock_ast::GenericParam {
17014        bock_ast::GenericParam {
17015            id,
17016            span: span(),
17017            name: ident(name),
17018            bounds: vec![],
17019        }
17020    }
17021
17022    fn named_type(id: u32, name: &str) -> AIRNode {
17023        node(
17024            id,
17025            NodeKind::TypeNamed {
17026                path: type_path(&[name]),
17027                args: vec![],
17028            },
17029        )
17030    }
17031
17032    /// `record Box[T] { value: T }`.
17033    fn generic_box_record() -> AIRNode {
17034        node(
17035            10,
17036            NodeKind::RecordDecl {
17037                annotations: vec![],
17038                visibility: Visibility::Private,
17039                name: ident("Box"),
17040                generic_params: vec![generic_param(11, "T")],
17041                fields: vec![bock_ast::RecordDeclField {
17042                    id: 12,
17043                    span: span(),
17044                    name: ident("value"),
17045                    ty: TypeExpr::Named {
17046                        id: 13,
17047                        span: span(),
17048                        path: type_path(&["T"]),
17049                        args: vec![],
17050                    },
17051                    default: None,
17052                }],
17053            },
17054        )
17055    }
17056
17057    /// `impl Box { fn get(self) -> T { return self.value } }`.
17058    fn generic_box_impl() -> AIRNode {
17059        let self_param = node(
17060            20,
17061            NodeKind::Param {
17062                pattern: Box::new(bind_pat(21, "self")),
17063                ty: None,
17064                default: None,
17065            },
17066        );
17067        let body = block(
17068            22,
17069            vec![],
17070            Some(node(
17071                23,
17072                NodeKind::Return {
17073                    value: Some(Box::new(node(
17074                        24,
17075                        NodeKind::FieldAccess {
17076                            object: Box::new(id_node(25, "self")),
17077                            field: ident("value"),
17078                        },
17079                    ))),
17080                },
17081            )),
17082        );
17083        let method = node(
17084            26,
17085            NodeKind::FnDecl {
17086                annotations: vec![],
17087                visibility: Visibility::Private,
17088                is_async: false,
17089                name: ident("get"),
17090                generic_params: vec![],
17091                params: vec![self_param],
17092                return_type: Some(Box::new(named_type(27, "T"))),
17093                effect_clause: vec![],
17094                where_clause: vec![],
17095                body: Box::new(body),
17096            },
17097        );
17098        node(
17099            30,
17100            NodeKind::ImplBlock {
17101                annotations: vec![],
17102                generic_params: vec![],
17103                trait_path: None,
17104                trait_args: vec![],
17105                target: Box::new(named_type(31, "Box")),
17106                where_clause: vec![],
17107                methods: vec![method],
17108            },
17109        )
17110    }
17111
17112    #[test]
17113    fn generic_method_receiver_carries_type_params() {
17114        // `impl Box { ... }` for `record Box[T]` must emit
17115        // `func (self *Box[T]) get() T` — Go requires the type-param list on the
17116        // receiver, recovered from the record decl since the impl has none.
17117        let out = gen(&module(
17118            vec![],
17119            vec![generic_box_record(), generic_box_impl()],
17120        ));
17121        assert!(
17122            out.contains("func (self *Box[T]) get() T {"),
17123            "generic method receiver should carry `[T]`, got: {out}"
17124        );
17125    }
17126
17127    /// `impl Box { fn map[U](self, f: Fn(T) -> U) -> Box[U] { Box { value:
17128    /// f(self.value) } } }`.
17129    fn generic_box_map_impl() -> AIRNode {
17130        let self_param = node(
17131            120,
17132            NodeKind::Param {
17133                pattern: Box::new(bind_pat(121, "self")),
17134                ty: None,
17135                default: None,
17136            },
17137        );
17138        // `f: Fn(T) -> U`
17139        let f_ty = node(
17140            122,
17141            NodeKind::TypeFunction {
17142                params: vec![named_type(123, "T")],
17143                ret: Box::new(named_type(124, "U")),
17144                effects: vec![],
17145            },
17146        );
17147        let f_param = node(
17148            125,
17149            NodeKind::Param {
17150                pattern: Box::new(bind_pat(126, "f")),
17151                ty: Some(Box::new(f_ty)),
17152                default: None,
17153            },
17154        );
17155        // Body: `Box { value: f(self.value) }`
17156        let call_f = node(
17157            127,
17158            NodeKind::Call {
17159                callee: Box::new(id_node(128, "f")),
17160                type_args: vec![],
17161                args: vec![AirArg {
17162                    label: None,
17163                    value: node(
17164                        129,
17165                        NodeKind::FieldAccess {
17166                            object: Box::new(id_node(130, "self")),
17167                            field: ident("value"),
17168                        },
17169                    ),
17170                }],
17171            },
17172        );
17173        let construct = node(
17174            131,
17175            NodeKind::RecordConstruct {
17176                path: type_path(&["Box"]),
17177                fields: vec![bock_air::AirRecordField {
17178                    name: ident("value"),
17179                    value: Some(Box::new(call_f)),
17180                }],
17181                spread: None,
17182            },
17183        );
17184        let body = block(132, vec![], Some(construct));
17185        let ret_ty = node(
17186            133,
17187            NodeKind::TypeNamed {
17188                path: type_path(&["Box"]),
17189                args: vec![named_type(134, "U")],
17190            },
17191        );
17192        let method = node(
17193            135,
17194            NodeKind::FnDecl {
17195                annotations: vec![],
17196                visibility: Visibility::Public,
17197                is_async: false,
17198                name: ident("map"),
17199                generic_params: vec![generic_param(136, "U")],
17200                params: vec![self_param, f_param],
17201                return_type: Some(Box::new(ret_ty)),
17202                effect_clause: vec![],
17203                where_clause: vec![],
17204                body: Box::new(body),
17205            },
17206        );
17207        node(
17208            137,
17209            NodeKind::ImplBlock {
17210                annotations: vec![],
17211                generic_params: vec![],
17212                trait_path: None,
17213                trait_args: vec![],
17214                target: Box::new(named_type(138, "Box")),
17215                where_clause: vec![],
17216                methods: vec![method],
17217            },
17218        )
17219    }
17220
17221    #[test]
17222    fn method_level_type_params_lower_to_free_function() {
17223        // DQ28: Go forbids method type params, so `Box[T].map[U]` lowers to a
17224        // free function `func Box_Map[T any, U any](self Box[T], f func(T) U)
17225        // Box[U]` (the receiver becomes a leading `self` parameter; the
17226        // receiver's `T` and the method's `U` combine on the free function).
17227        let out = gen(&module(
17228            vec![],
17229            vec![generic_box_record(), generic_box_map_impl()],
17230        ));
17231        assert!(
17232            out.contains("func Box_Map[T any, U any](self Box[T], f func(T) U) Box[U] {"),
17233            "method-generic should free-function-lower with combined type params, got: {out}"
17234        );
17235        // The invalid `func (self *Box[T]) Map[U](..)` (Go syntax error) must NOT
17236        // be emitted.
17237        assert!(
17238            !out.contains(") Map["),
17239            "must not emit a Go method with type params, got: {out}"
17240        );
17241    }
17242
17243    #[test]
17244    fn method_level_type_param_call_site_rewrites_to_free_function() {
17245        // A call `b.map(f)` to the free-function-lowered `Box.map[U]` rewrites to
17246        // `Box_Map(b, f)` (receiver-first), for both the `MethodCall` and the
17247        // desugared `Call(FieldAccess(b, map), [b, f])` shapes.
17248        let recv = id_node(200, "b");
17249        let cb = node(
17250            201,
17251            NodeKind::Lambda {
17252                params: vec![param_node(202, "x")],
17253                body: Box::new(block(
17254                    203,
17255                    vec![],
17256                    Some(node(
17257                        204,
17258                        NodeKind::BinaryOp {
17259                            op: BinOp::Mul,
17260                            left: Box::new(id_node(205, "x")),
17261                            right: Box::new(int_lit(206, "2")),
17262                        },
17263                    )),
17264                )),
17265            },
17266        );
17267        let call = node(
17268            207,
17269            NodeKind::MethodCall {
17270                receiver: Box::new(recv),
17271                method: ident("map"),
17272                type_args: vec![],
17273                args: vec![AirArg {
17274                    label: None,
17275                    value: cb,
17276                }],
17277            },
17278        );
17279        let let_stmt = node(
17280            208,
17281            NodeKind::LetBinding {
17282                is_mut: false,
17283                pattern: Box::new(bind_pat(209, "r")),
17284                ty: None,
17285                value: Box::new(call),
17286            },
17287        );
17288        let out = gen(&module(
17289            vec![],
17290            vec![generic_box_record(), generic_box_map_impl(), let_stmt],
17291        ));
17292        assert!(
17293            out.contains("Box_Map(b, "),
17294            "call site should rewrite to the free-function call `Box_Map(b, ..)`, got: {out}"
17295        );
17296        assert!(
17297            !out.contains("b.Map("),
17298            "call site must not keep the Go method-call form, got: {out}"
17299        );
17300    }
17301
17302    #[test]
17303    fn generic_construct_emits_explicit_type_args() {
17304        // `Box { value: 42 }` → `Box[int64]{Value: 42}` (Go does not infer
17305        // struct type args from composite-literal fields).
17306        let construct = node(
17307            40,
17308            NodeKind::RecordConstruct {
17309                path: type_path(&["Box"]),
17310                fields: vec![bock_air::AirRecordField {
17311                    name: ident("value"),
17312                    value: Some(Box::new(int_lit(41, "42"))),
17313                }],
17314                spread: None,
17315            },
17316        );
17317        let let_stmt = node(
17318            42,
17319            NodeKind::LetBinding {
17320                is_mut: false,
17321                pattern: Box::new(bind_pat(43, "b")),
17322                ty: None,
17323                value: Box::new(construct),
17324            },
17325        );
17326        let out = gen(&module(vec![], vec![generic_box_record(), let_stmt]));
17327        assert!(
17328            out.contains("Box[int64]{Value: 42}"),
17329            "generic construction should carry explicit `[int64]`, got: {out}"
17330        );
17331    }
17332
17333    #[test]
17334    fn generic_fn_return_list_literal_uses_param_type() {
17335        // GAP-C: `fn single[T](x: T) -> List[T] { return [x] }` must emit
17336        // `return []T{x}`, not `[]interface{}{x}` (which a `[]T` return rejects).
17337        let list_t = node(
17338            61,
17339            NodeKind::TypeNamed {
17340                path: type_path(&["List"]),
17341                args: vec![named_type(62, "T")],
17342            },
17343        );
17344        let body = block(
17345            63,
17346            vec![],
17347            Some(node(
17348                64,
17349                NodeKind::Return {
17350                    value: Some(Box::new(node(
17351                        65,
17352                        NodeKind::ListLiteral {
17353                            elems: vec![id_node(66, "x")],
17354                        },
17355                    ))),
17356                },
17357            )),
17358        );
17359        let f = node(
17360            67,
17361            NodeKind::FnDecl {
17362                annotations: vec![],
17363                visibility: Visibility::Private,
17364                is_async: false,
17365                name: ident("single"),
17366                generic_params: vec![generic_param(68, "T")],
17367                params: vec![node(
17368                    69,
17369                    NodeKind::Param {
17370                        pattern: Box::new(bind_pat(70, "x")),
17371                        ty: Some(Box::new(named_type(71, "T"))),
17372                        default: None,
17373                    },
17374                )],
17375                return_type: Some(Box::new(list_t)),
17376                effect_clause: vec![],
17377                where_clause: vec![],
17378                body: Box::new(body),
17379            },
17380        );
17381        let out = gen(&module(vec![], vec![f]));
17382        assert!(
17383            out.contains("return []T{x}"),
17384            "generic fn returning a list literal should use `[]T`, got: {out}"
17385        );
17386    }
17387
17388    #[test]
17389    fn generic_construct_uses_declared_type_args_for_nested_param() {
17390        // GAP-C/D plumbing: `let c: ListIter[Int] = ListIter { xs: [...] }` for
17391        // `record ListIter[T] { xs: List[T] }` must emit `ListIter[int64]{...}`.
17392        // Field inference yields `any` here (no field is typed exactly `T`; `xs`
17393        // is `List[T]`), so the construction must adopt the declared binding
17394        // type's concrete args.
17395        let record = node(
17396            10,
17397            NodeKind::RecordDecl {
17398                annotations: vec![],
17399                visibility: Visibility::Private,
17400                name: ident("ListIter"),
17401                generic_params: vec![generic_param(11, "T")],
17402                fields: vec![bock_ast::RecordDeclField {
17403                    id: 12,
17404                    span: span(),
17405                    name: ident("xs"),
17406                    ty: TypeExpr::Named {
17407                        id: 13,
17408                        span: span(),
17409                        path: type_path(&["List"]),
17410                        args: vec![TypeExpr::Named {
17411                            id: 14,
17412                            span: span(),
17413                            path: type_path(&["T"]),
17414                            args: vec![],
17415                        }],
17416                    },
17417                    default: None,
17418                }],
17419            },
17420        );
17421        let construct = node(
17422            20,
17423            NodeKind::RecordConstruct {
17424                path: type_path(&["ListIter"]),
17425                fields: vec![bock_air::AirRecordField {
17426                    name: ident("xs"),
17427                    value: Some(Box::new(node(
17428                        21,
17429                        NodeKind::ListLiteral {
17430                            elems: vec![int_lit(22, "1")],
17431                        },
17432                    ))),
17433                }],
17434                spread: None,
17435            },
17436        );
17437        let let_stmt = node(
17438            23,
17439            NodeKind::LetBinding {
17440                is_mut: false,
17441                pattern: Box::new(bind_pat(24, "c")),
17442                ty: Some(Box::new(node(
17443                    25,
17444                    NodeKind::TypeNamed {
17445                        path: type_path(&["ListIter"]),
17446                        args: vec![named_type(26, "Int")],
17447                    },
17448                ))),
17449                value: Box::new(construct),
17450            },
17451        );
17452        let out = gen(&module(vec![], vec![record, let_stmt]));
17453        assert!(
17454            out.contains("ListIter[int64]{"),
17455            "construction should adopt the declared binding type's `[int64]`, got: {out}"
17456        );
17457        assert!(
17458            !out.contains("ListIter[any]{"),
17459            "construction must NOT fall back to `[any]` when a declared type is present, got: {out}"
17460        );
17461    }
17462
17463    #[test]
17464    fn lambda_return_type_inferred_from_body() {
17465        // `(n: Int) => n + 1` → `func(n int64) int64 { return (n + 1) }`, not
17466        // `interface{}` (which fails to satisfy a typed `func(int64) int64`).
17467        let lambda = node(
17468            50,
17469            NodeKind::Lambda {
17470                params: vec![typed_param_node(51, "n", "Int")],
17471                body: Box::new(node(
17472                    52,
17473                    NodeKind::BinaryOp {
17474                        op: BinOp::Add,
17475                        left: Box::new(id_node(53, "n")),
17476                        right: Box::new(int_lit(54, "1")),
17477                    },
17478                )),
17479            },
17480        );
17481        let let_stmt = node(
17482            55,
17483            NodeKind::LetBinding {
17484                is_mut: false,
17485                pattern: Box::new(bind_pat(56, "inc")),
17486                ty: None,
17487                value: Box::new(lambda),
17488            },
17489        );
17490        let out = gen(&module(vec![], vec![let_stmt]));
17491        assert!(
17492            out.contains("func(n int64) int64 {"),
17493            "lambda should infer `int64` return type, got: {out}"
17494        );
17495        assert!(
17496            !out.contains("func(n int64) interface{}"),
17497            "lambda should NOT fall back to interface{{}} return, got: {out}"
17498        );
17499    }
17500
17501    #[test]
17502    fn unify_go_pattern_binds_iterator_and_slice_params() {
17503        let gp = vec!["T".to_string(), "U".to_string()];
17504        // `ListIterator[T]` against `ListIterator[int64]` binds T → int64.
17505        let mut b = HashMap::new();
17506        GoEmitCtx::unify_go_pattern("ListIterator[T]", "ListIterator[int64]", &gp, &mut b);
17507        assert_eq!(b.get("T"), Some(&"int64".to_string()));
17508        // `[]T` against `[]string` binds T → string.
17509        let mut b2 = HashMap::new();
17510        GoEmitCtx::unify_go_pattern("[]T", "[]string", &gp, &mut b2);
17511        assert_eq!(b2.get("T"), Some(&"string".to_string()));
17512        // A bare param binds to the whole concrete type.
17513        let mut b3 = HashMap::new();
17514        GoEmitCtx::unify_go_pattern("U", "map[string]int64", &gp, &mut b3);
17515        assert_eq!(b3.get("U"), Some(&"map[string]int64".to_string()));
17516        // A structural mismatch records nothing (conservative).
17517        let mut b4 = HashMap::new();
17518        GoEmitCtx::unify_go_pattern("ListIterator[T]", "int64", &gp, &mut b4);
17519        assert!(b4.is_empty());
17520    }
17521
17522    #[test]
17523    fn split_top_level_commas_respects_nesting() {
17524        assert_eq!(
17525            GoEmitCtx::split_top_level_commas("int64, []string"),
17526            vec!["int64".to_string(), "[]string".to_string()]
17527        );
17528        // A comma nested inside `[...]` is not a top-level separator.
17529        assert_eq!(
17530            GoEmitCtx::split_top_level_commas("map[string]int64, T"),
17531            vec!["map[string]int64".to_string(), "T".to_string()]
17532        );
17533        assert_eq!(
17534            GoEmitCtx::split_top_level_commas("int64"),
17535            vec!["int64".to_string()]
17536        );
17537    }
17538
17539    #[test]
17540    fn replace_type_token_only_swaps_whole_identifiers() {
17541        // `T` in `[]T` is replaced; `T` inside `Tree` is not.
17542        assert_eq!(
17543            GoEmitCtx::replace_type_token("[]T", "T", "int64"),
17544            "[]int64"
17545        );
17546        assert_eq!(GoEmitCtx::replace_type_token("Tree", "T", "int64"), "Tree");
17547        assert_eq!(
17548            GoEmitCtx::replace_type_token("func(T) T", "T", "int64"),
17549            "func(int64) int64"
17550        );
17551        assert!(GoEmitCtx::contains_type_token("ListIterator[T]", "T"));
17552        assert!(!GoEmitCtx::contains_type_token("ListIterator[int64]", "T"));
17553    }
17554
17555    #[test]
17556    fn generic_trait_with_type_param_is_a_generic_interface() {
17557        // A trait that declares its own generic param (`Iterable[T]`) becomes a
17558        // generic Go interface so a method signature naming `T` is in scope.
17559        let method = node(
17560            2,
17561            NodeKind::FnDecl {
17562                annotations: vec![],
17563                visibility: Visibility::Public,
17564                is_async: false,
17565                name: ident("iter"),
17566                generic_params: vec![],
17567                params: vec![node(
17568                    10,
17569                    NodeKind::Param {
17570                        pattern: Box::new(bind_pat(11, "self")),
17571                        ty: None,
17572                        default: None,
17573                    },
17574                )],
17575                return_type: Some(Box::new(node(
17576                    20,
17577                    NodeKind::TypeNamed {
17578                        path: type_path(&["ListIterator"]),
17579                        args: vec![named_type(21, "T")],
17580                    },
17581                ))),
17582                effect_clause: vec![],
17583                where_clause: vec![],
17584                body: Box::new(block(3, vec![], None)),
17585            },
17586        );
17587        let t = node(
17588            1,
17589            NodeKind::TraitDecl {
17590                annotations: vec![],
17591                visibility: Visibility::Public,
17592                is_platform: false,
17593                name: ident("Iterable"),
17594                generic_params: vec![generic_param(30, "T")],
17595                associated_types: vec![],
17596                methods: vec![method],
17597            },
17598        );
17599        let out = gen(&module(vec![], vec![t]));
17600        assert!(
17601            out.contains("type Iterable[T any] interface {"),
17602            "generic trait should carry its type param on the interface, got: {out}"
17603        );
17604        assert!(
17605            out.contains("Iter() ListIterator[T]"),
17606            "the method return must keep `[T]`, got: {out}"
17607        );
17608    }
17609
17610    // ── Per-module native-package tree (S3) ─────────────────────────────────
17611
17612    fn mod_path(segs: &[&str]) -> bock_ast::ModulePath {
17613        bock_ast::ModulePath {
17614            segments: segs.iter().map(|s| ident(s)).collect(),
17615            span: span(),
17616        }
17617    }
17618
17619    /// A module node with a declared dotted `path` (e.g. `core.option`).
17620    fn module_with_path(path: &[&str], imports: Vec<AIRNode>, items: Vec<AIRNode>) -> AIRNode {
17621        node(
17622            0,
17623            NodeKind::Module {
17624                path: Some(mod_path(path)),
17625                annotations: vec![],
17626                imports,
17627                items,
17628            },
17629        )
17630    }
17631
17632    /// An `import <path>.{ name }` AIR node (a single-item `Named` import).
17633    fn import_named(id: u32, path: &[&str], name: &str) -> AIRNode {
17634        node(
17635            id,
17636            NodeKind::ImportDecl {
17637                path: mod_path(path),
17638                items: bock_ast::ImportItems::Named(vec![bock_ast::ImportedName {
17639                    span: span(),
17640                    name: ident(name),
17641                    alias: None,
17642                }]),
17643            },
17644        )
17645    }
17646
17647    /// A bare `fn <name>() -> <tail>` declaration with the given visibility.
17648    fn fn_decl_tail(id: u32, vis: Visibility, name: &str, tail: AIRNode) -> AIRNode {
17649        node(
17650            id,
17651            NodeKind::FnDecl {
17652                annotations: vec![],
17653                visibility: vis,
17654                is_async: false,
17655                name: ident(name),
17656                generic_params: vec![],
17657                params: vec![],
17658                return_type: None,
17659                effect_clause: vec![],
17660                where_clause: vec![],
17661                body: Box::new(block(id + 1, vec![], Some(tail))),
17662            },
17663        )
17664    }
17665
17666    #[test]
17667    fn per_module_emits_native_go_package_tree() {
17668        // entry `module main` uses `mathutil.add_one`; `module mathutil` exports
17669        // a `public fn add_one`. Per-module emission must produce the native Go
17670        // *source* package: `main.go` (one `package main`) and the flat
17671        // `mathutil.go` (same package — the call site needs no import) —
17672        // separate files, not a single collapsed file. The `go.mod` run
17673        // affordance is emitted by the scaffolder (project mode), NOT codegen
17674        // (S6a / DV18).
17675        let call = node(
17676            10,
17677            NodeKind::Call {
17678                callee: Box::new(id_node(11, "add_one")),
17679                args: vec![AirArg {
17680                    label: None,
17681                    value: int_lit(12, "6"),
17682                }],
17683                type_args: vec![],
17684            },
17685        );
17686        let main_mod = module_with_path(
17687            &["main"],
17688            vec![import_named(5, &["mathutil"], "add_one")],
17689            vec![fn_decl_tail(1, Visibility::Private, "main", call)],
17690        );
17691        let util_mod = module_with_path(
17692            &["mathutil"],
17693            vec![],
17694            vec![fn_decl_tail(
17695                20,
17696                Visibility::Public,
17697                "add_one",
17698                int_lit(22, "7"),
17699            )],
17700        );
17701
17702        let gen = GoGenerator::new();
17703        let out = gen
17704            .generate_project(&[
17705                (&main_mod, std::path::Path::new("src/main.bock")),
17706                (&util_mod, std::path::Path::new("src/mathutil.bock")),
17707            ])
17708            .unwrap();
17709        let by_name = |p: &str| out.files.iter().find(|f| f.path == std::path::Path::new(p));
17710        let main_file = by_name("main.go").expect("main.go emitted");
17711        let util_file = by_name("mathutil.go").expect("flat mathutil.go emitted");
17712        // Codegen no longer emits the manifest (S6a / DV18) — the scaffolder
17713        // owns the `go.mod` in project mode.
17714        assert!(
17715            by_name("go.mod").is_none(),
17716            "codegen must NOT emit go.mod — the scaffolder owns it (S6a)"
17717        );
17718
17719        assert!(
17720            main_file.content.starts_with("package main"),
17721            "main.go must be `package main`; got:\n{}",
17722            main_file.content
17723        );
17724        // Same package → the cross-module call needs no import statement.
17725        assert!(
17726            !main_file.content.contains("import \"mathutil\""),
17727            "main.go must NOT import the sibling (same package); got:\n{}",
17728            main_file.content
17729        );
17730        // The exported fn is PascalCased on emit; the call site matches.
17731        assert!(
17732            util_file.content.contains("func AddOne("),
17733            "mathutil.go must carry the exported fn; got:\n{}",
17734            util_file.content
17735        );
17736        assert!(
17737            main_file.content.contains("AddOne("),
17738            "main.go must call the cross-module fn; got:\n{}",
17739            main_file.content
17740        );
17741    }
17742
17743    #[test]
17744    fn per_module_nested_module_flattens_filename() {
17745        // A nested `core.option` module flattens to a single flat file
17746        // `core.option.go` (one package per dir — no subdirectory; dots kept so
17747        // a `core.test` module never collides with Go's `_test.go` suffix).
17748        let opt_mod = module_with_path(
17749            &["core", "option"],
17750            vec![],
17751            vec![fn_decl_tail(
17752                20,
17753                Visibility::Public,
17754                "get_or",
17755                int_lit(22, "0"),
17756            )],
17757        );
17758        let main_mod = module_with_path(
17759            &["main"],
17760            vec![import_named(5, &["core", "option"], "get_or")],
17761            vec![fn_decl_tail(
17762                1,
17763                Visibility::Private,
17764                "main",
17765                node(
17766                    10,
17767                    NodeKind::Call {
17768                        callee: Box::new(id_node(11, "get_or")),
17769                        args: vec![],
17770                        type_args: vec![],
17771                    },
17772                ),
17773            )],
17774        );
17775        let gen = GoGenerator::new();
17776        let out = gen
17777            .generate_project(&[
17778                (&main_mod, std::path::Path::new("src/main.bock")),
17779                (&opt_mod, std::path::Path::new("src/core/option.bock")),
17780            ])
17781            .unwrap();
17782        let by_name = |p: &str| out.files.iter().find(|f| f.path == std::path::Path::new(p));
17783        by_name("core.option.go").expect("nested module flattens to core.option.go");
17784        // No subdirectory: there must be no `core/option.go`.
17785        assert!(
17786            by_name("core/option.go").is_none(),
17787            "go must NOT emit a subdirectory package file"
17788        );
17789    }
17790
17791    /// `fn f() { let x = if (c) { 1 } else { return 0 }  x }` — value-position
17792    /// `if` with a diverging else. The shared value-CF hoist lowers it to a
17793    /// `var __bockCf0 T` (type inferred from the assigned arm values) plus
17794    /// statement-form assignment, never `/* unsupported */` or an IIFE that
17795    /// captures the `return`.
17796    fn diverging_value_if_fn() -> AIRNode {
17797        let then_b = block(2, vec![], Some(int_lit(3, "1")));
17798        let ret = node(
17799            5,
17800            NodeKind::Return {
17801                value: Some(Box::new(int_lit(6, "0"))),
17802            },
17803        );
17804        let else_b = block(4, vec![], Some(ret));
17805        let if_node = node(
17806            1,
17807            NodeKind::If {
17808                let_pattern: None,
17809                condition: Box::new(id_node(7, "c")),
17810                then_block: Box::new(then_b),
17811                else_block: Some(Box::new(else_b)),
17812            },
17813        );
17814        let let_x = node(
17815            10,
17816            NodeKind::LetBinding {
17817                is_mut: false,
17818                pattern: Box::new(bind_pat(11, "x")),
17819                ty: None,
17820                value: Box::new(if_node),
17821            },
17822        );
17823        let body = block(20, vec![let_x], Some(id_node(21, "x")));
17824        let f = node(
17825            30,
17826            NodeKind::FnDecl {
17827                annotations: vec![],
17828                visibility: Visibility::Private,
17829                is_async: false,
17830                name: ident("f"),
17831                generic_params: vec![],
17832                params: vec![],
17833                return_type: None,
17834                effect_clause: vec![],
17835                where_clause: vec![],
17836                body: Box::new(body),
17837            },
17838        );
17839        module(vec![], vec![f])
17840    }
17841
17842    #[test]
17843    fn diverging_value_if_hoists_to_stmt_form_no_unsupported() {
17844        let out = gen(&diverging_value_if_fn());
17845        assert!(
17846            !out.contains("/* unsupported */"),
17847            "diverging value-if must not emit `/* unsupported */`, got: {out}"
17848        );
17849        // The temp is declared with an inferred Go type (`int64` from the arms).
17850        // Go's `go_value_ident` strips the leading `__`, so the name is `bockCf0`.
17851        assert!(
17852            out.contains("var bockCf0 int64"),
17853            "must declare a typed temp `var bockCf0 int64`, got: {out}"
17854        );
17855        assert!(
17856            out.contains("bockCf0 = 1"),
17857            "value arm must assign the temp, got: {out}"
17858        );
17859        assert!(
17860            out.contains("return 0"),
17861            "diverging arm must keep its return, got: {out}"
17862        );
17863    }
17864
17865    // ── Q-guard-let-shared (go) ───────────────────────────────────────────────
17866
17867    /// `guard (let Ok(v) = cond) else { return … }` must test the discriminant
17868    /// tag and bind the payload into the *enclosing* scope (live after the
17869    /// guard), not negate the non-bool `__bockResult` and drop the binding.
17870    #[test]
17871    fn go_guard_let_tests_tag_and_binds_payload() {
17872        // guard (let Ok(v) = res) else { return }
17873        let guard = node(
17874            10,
17875            NodeKind::Guard {
17876                let_pattern: Some(Box::new(node(
17877                    11,
17878                    NodeKind::ConstructorPat {
17879                        path: type_path(&["Ok"]),
17880                        fields: vec![bind_pat(12, "v")],
17881                    },
17882                ))),
17883                condition: Box::new(id_node(13, "res")),
17884                else_block: Box::new(block(
17885                    14,
17886                    vec![node(15, NodeKind::Return { value: None })],
17887                    None,
17888                )),
17889            },
17890        );
17891        // fn check() -> Void { guard …; }
17892        let f = node(
17893            20,
17894            NodeKind::FnDecl {
17895                annotations: vec![],
17896                visibility: Visibility::Private,
17897                is_async: false,
17898                name: ident("check"),
17899                generic_params: vec![],
17900                params: vec![],
17901                return_type: None,
17902                effect_clause: vec![],
17903                where_clause: vec![],
17904                body: Box::new(block(21, vec![guard], None)),
17905            },
17906        );
17907        let out = gen(&module(vec![], vec![f]));
17908        // The discriminant is hoisted into a `__guard` temp and tested by tag,
17909        // never negated as a bool.
17910        assert!(
17911            out.contains("__guard0 := res"),
17912            "guard-let must hoist the discriminant, got: {out}"
17913        );
17914        assert!(
17915            out.contains("if !(__guard0.tag == \"Ok\")"),
17916            "guard-let must test the tag, got: {out}"
17917        );
17918        assert!(
17919            !out.contains("if !(res)"),
17920            "guard-let must not negate the non-bool discriminant, got: {out}"
17921        );
17922        // The else arm diverges, then the payload binding lands after the `if`.
17923        assert!(
17924            out.contains("v := __guard0.v"),
17925            "guard-let must bind the payload after the guard, got: {out}"
17926        );
17927    }
17928
17929    // ── Q-let-shadow-const (go) ───────────────────────────────────────────────
17930
17931    /// A shadowing `let` re-binding a name already declared in the block lowers
17932    /// to a reassignment (`acc = …`), not a colliding re-declaration
17933    /// (`acc := …` / `var acc … = …` — Go's "no new variables on left side").
17934    #[test]
17935    fn go_let_shadow_rebinds_as_assignment() {
17936        let stmts = vec![
17937            // let acc = 1
17938            node(
17939                10,
17940                NodeKind::LetBinding {
17941                    is_mut: false,
17942                    pattern: Box::new(bind_pat(11, "acc")),
17943                    ty: None,
17944                    value: Box::new(int_lit(12, "1")),
17945                },
17946            ),
17947            // let acc = acc + 2  (shadow → reassignment)
17948            node(
17949                13,
17950                NodeKind::LetBinding {
17951                    is_mut: false,
17952                    pattern: Box::new(bind_pat(14, "acc")),
17953                    ty: None,
17954                    value: Box::new(node(
17955                        15,
17956                        NodeKind::BinaryOp {
17957                            op: BinOp::Add,
17958                            left: Box::new(id_node(16, "acc")),
17959                            right: Box::new(int_lit(17, "2")),
17960                        },
17961                    )),
17962                },
17963            ),
17964        ];
17965        let f = node(
17966            20,
17967            NodeKind::FnDecl {
17968                annotations: vec![],
17969                visibility: Visibility::Private,
17970                is_async: false,
17971                name: ident("run"),
17972                generic_params: vec![],
17973                params: vec![],
17974                return_type: None,
17975                effect_clause: vec![],
17976                where_clause: vec![],
17977                body: Box::new(block(21, stmts, Some(id_node(22, "acc")))),
17978            },
17979        );
17980        let out = gen(&module(vec![], vec![f]));
17981        // First binding declares; the second reassigns.
17982        assert!(
17983            out.contains("acc = (acc + 2)"),
17984            "the shadowing re-bind must be an assignment, got: {out}"
17985        );
17986        // No second declaration of `acc`.
17987        assert!(
17988            out.matches("acc :=").count() + out.matches("var acc").count() <= 1,
17989            "a shadowing re-bind must not re-declare `acc`, got: {out}"
17990        );
17991    }
17992
17993    /// A `let` shadowing a *parameter* (the same Go scope as the body) must also
17994    /// reassign, not re-declare.
17995    #[test]
17996    fn go_let_shadow_of_param_rebinds_as_assignment() {
17997        // fn bump(n) { let n = n + 1; n }
17998        let let_n = node(
17999            10,
18000            NodeKind::LetBinding {
18001                is_mut: false,
18002                pattern: Box::new(bind_pat(11, "n")),
18003                ty: None,
18004                value: Box::new(node(
18005                    12,
18006                    NodeKind::BinaryOp {
18007                        op: BinOp::Add,
18008                        left: Box::new(id_node(13, "n")),
18009                        right: Box::new(int_lit(14, "1")),
18010                    },
18011                )),
18012            },
18013        );
18014        let f = node(
18015            20,
18016            NodeKind::FnDecl {
18017                annotations: vec![],
18018                visibility: Visibility::Private,
18019                is_async: false,
18020                name: ident("bump"),
18021                generic_params: vec![],
18022                params: vec![typed_param_node(21, "n", "Int")],
18023                return_type: Some(Box::new(node(
18024                    22,
18025                    NodeKind::TypeNamed {
18026                        path: type_path(&["Int"]),
18027                        args: vec![],
18028                    },
18029                ))),
18030                effect_clause: vec![],
18031                where_clause: vec![],
18032                body: Box::new(block(23, vec![let_n], Some(id_node(24, "n")))),
18033            },
18034        );
18035        let out = gen(&module(vec![], vec![f]));
18036        assert!(
18037            out.contains("n = (n + 1)"),
18038            "a `let` shadowing a param must reassign, got: {out}"
18039        );
18040        assert!(
18041            !out.contains("n := (n + 1)") && !out.contains("var n int64 = (n + 1)"),
18042            "a `let` shadowing a param must not re-declare, got: {out}"
18043        );
18044    }
18045
18046    // ── Q-propagate-operator-noop (go) ────────────────────────────────────────
18047
18048    /// `let v = expr?` must unwrap the `Ok`/`Some` payload and early-return the
18049    /// propagated error/None on failure — not pass `expr` through unchanged
18050    /// (which bound the whole `__bockResult` and never short-circuited).
18051    #[test]
18052    fn go_propagate_let_unwraps_and_early_returns() {
18053        // let v = f()?
18054        let let_v = node(
18055            10,
18056            NodeKind::LetBinding {
18057                is_mut: false,
18058                pattern: Box::new(bind_pat(11, "v")),
18059                ty: None,
18060                value: Box::new(node(
18061                    12,
18062                    NodeKind::Propagate {
18063                        expr: Box::new(node(
18064                            13,
18065                            NodeKind::Call {
18066                                callee: Box::new(id_node(14, "f")),
18067                                args: vec![],
18068                                type_args: vec![],
18069                            },
18070                        )),
18071                    },
18072                )),
18073            },
18074        );
18075        // fn g() -> Result[Int, String] { let v = f()?; Ok(v) }
18076        let body_tail = node(
18077            15,
18078            NodeKind::Call {
18079                callee: Box::new(id_node(16, "Ok")),
18080                args: vec![AirArg {
18081                    label: None,
18082                    value: id_node(17, "v"),
18083                }],
18084                type_args: vec![],
18085            },
18086        );
18087        let f = node(
18088            20,
18089            NodeKind::FnDecl {
18090                annotations: vec![],
18091                visibility: Visibility::Private,
18092                is_async: false,
18093                name: ident("g"),
18094                generic_params: vec![],
18095                params: vec![],
18096                return_type: Some(Box::new(node(
18097                    22,
18098                    NodeKind::TypeNamed {
18099                        path: type_path(&["Result"]),
18100                        args: vec![
18101                            node(
18102                                23,
18103                                NodeKind::TypeNamed {
18104                                    path: type_path(&["Int"]),
18105                                    args: vec![],
18106                                },
18107                            ),
18108                            node(
18109                                24,
18110                                NodeKind::TypeNamed {
18111                                    path: type_path(&["String"]),
18112                                    args: vec![],
18113                                },
18114                            ),
18115                        ],
18116                    },
18117                ))),
18118                effect_clause: vec![],
18119                where_clause: vec![],
18120                body: Box::new(block(21, vec![let_v], Some(body_tail))),
18121            },
18122        );
18123        let out = gen(&module(vec![], vec![f]));
18124        // The operand is hoisted; failure early-returns the propagated Err.
18125        assert!(
18126            out.contains("__try0 := f()"),
18127            "propagate must hoist the operand, got: {out}"
18128        );
18129        assert!(
18130            out.contains("if __try0.tag == \"Err\""),
18131            "propagate must check the Err tag, got: {out}"
18132        );
18133        assert!(
18134            out.contains("return __try0"),
18135            "propagate must early-return the propagated Err, got: {out}"
18136        );
18137        // The success payload is bound to `v` (not the whole `__bockResult`).
18138        assert!(
18139            out.contains("v := __try0.v") || out.contains("v := __bockAsInt64(__try0.v)"),
18140            "propagate must bind the unwrapped payload, got: {out}"
18141        );
18142        // It is no longer a no-op passthrough.
18143        assert!(
18144            !out.contains("v := f()\n"),
18145            "propagate must not pass the operand through unchanged, got: {out}"
18146        );
18147    }
18148
18149    /// Build a single-param fn whose body is `return match scrutinee { arms }`,
18150    /// for exercising the expression-position match lowering.
18151    fn return_match_fn(name: &str, param: &str, ty: &str, arms: Vec<AIRNode>) -> AIRNode {
18152        let match_node = node(
18153            500,
18154            NodeKind::Match {
18155                scrutinee: Box::new(id_node(501, param)),
18156                arms,
18157            },
18158        );
18159        let ret = node(
18160            502,
18161            NodeKind::Return {
18162                value: Some(Box::new(match_node)),
18163            },
18164        );
18165        node(
18166            1,
18167            NodeKind::FnDecl {
18168                annotations: vec![],
18169                visibility: Visibility::Public,
18170                is_async: false,
18171                name: ident(name),
18172                generic_params: vec![],
18173                params: vec![typed_param_node(2, param, ty)],
18174                return_type: Some(Box::new(node(
18175                    3,
18176                    NodeKind::TypeNamed {
18177                        path: type_path(&["String"]),
18178                        args: vec![],
18179                    },
18180                ))),
18181                effect_clause: vec![],
18182                where_clause: vec![],
18183                body: Box::new(block(4, vec![], Some(ret))),
18184            },
18185        )
18186    }
18187
18188    /// Q-list-range-pattern-shared: a list-pattern `match` in expression position
18189    /// (`return match items { [] => …; [only] => …; [first, ..rest] => … }`) must
18190    /// route to the if-chain (the shared recogniser now flags `ListPat`), emitting
18191    /// a `len(...)` test per arm and positional element / `..rest` slice binds —
18192    /// not the broken `switch` whose every arm collapsed to `case interface{}:`.
18193    #[test]
18194    fn go_list_pattern_expr_match_lowers_to_ifchain_with_binds() {
18195        let empty_arm = node(
18196            20,
18197            NodeKind::MatchArm {
18198                pattern: Box::new(node(
18199                    21,
18200                    NodeKind::ListPat {
18201                        elems: vec![],
18202                        rest: None,
18203                    },
18204                )),
18205                guard: None,
18206                body: Box::new(block(22, vec![], Some(str_lit(23, "empty")))),
18207            },
18208        );
18209        let single_arm = node(
18210            30,
18211            NodeKind::MatchArm {
18212                pattern: Box::new(node(
18213                    31,
18214                    NodeKind::ListPat {
18215                        elems: vec![bind_pat(32, "only")],
18216                        rest: None,
18217                    },
18218                )),
18219                guard: None,
18220                body: Box::new(block(33, vec![], Some(id_node(34, "only")))),
18221            },
18222        );
18223        let head_rest_arm = node(
18224            40,
18225            NodeKind::MatchArm {
18226                pattern: Box::new(node(
18227                    41,
18228                    NodeKind::ListPat {
18229                        elems: vec![bind_pat(42, "first")],
18230                        rest: Some(Box::new(bind_pat(43, "rest"))),
18231                    },
18232                )),
18233                guard: None,
18234                body: Box::new(block(44, vec![], Some(id_node(45, "first")))),
18235            },
18236        );
18237        let else_arm = node(
18238            46,
18239            NodeKind::MatchArm {
18240                pattern: Box::new(node(47, NodeKind::WildcardPat)),
18241                guard: None,
18242                body: Box::new(block(48, vec![], Some(str_lit(49, "other")))),
18243            },
18244        );
18245        let f = return_match_fn(
18246            "DescribeList",
18247            "items",
18248            "List",
18249            vec![empty_arm, single_arm, head_rest_arm, else_arm],
18250        );
18251        let out = gen(&module(vec![], vec![f]));
18252        // No broken `case interface{}:` placeholder.
18253        assert!(
18254            !out.contains("case interface{}"),
18255            "list-pattern match must not emit a broken `case interface{{}}`, got: {out}"
18256        );
18257        // `[]` → exact length 0.
18258        assert!(
18259            out.contains("len(items) == 0"),
18260            "`[]` arm should test len == 0, got: {out}"
18261        );
18262        // `[only]` → length 1 and binds `only := items[0]`.
18263        assert!(
18264            out.contains("len(items) == 1") && out.contains("only := items[0]"),
18265            "`[only]` should test len == 1 and bind `only`, got: {out}"
18266        );
18267        // `[first, ..rest]` → length >= 1, binds first and a rest slice.
18268        assert!(
18269            out.contains("len(items) >= 1"),
18270            "`[first, ..rest]` should test len >= 1, got: {out}"
18271        );
18272        assert!(
18273            out.contains("first := items[0]") && out.contains("rest := items[1:]"),
18274            "`[first, ..rest]` should bind `first` and `rest := items[1:]`, got: {out}"
18275        );
18276        // The arm bodies return their values (expression-position IIFE).
18277        assert!(
18278            out.contains("return \"empty\""),
18279            "arm bodies must return their value, got: {out}"
18280        );
18281    }
18282
18283    /// Q-list-range-pattern-shared: a range-pattern `match` in expression position
18284    /// must route to the if-chain with a relational bounds test (`>= lo && < hi`
18285    /// exclusive, `<=` inclusive) — not a broken `switch`.
18286    #[test]
18287    fn go_range_pattern_expr_match_lowers_to_ifchain_with_bounds() {
18288        let lo_arm = node(
18289            20,
18290            NodeKind::MatchArm {
18291                pattern: Box::new(node(
18292                    21,
18293                    NodeKind::RangePat {
18294                        lo: Box::new(int_lit(22, "1")),
18295                        hi: Box::new(int_lit(23, "10")),
18296                        inclusive: false,
18297                    },
18298                )),
18299                guard: None,
18300                body: Box::new(block(24, vec![], Some(str_lit(25, "a")))),
18301            },
18302        );
18303        let hi_arm = node(
18304            30,
18305            NodeKind::MatchArm {
18306                pattern: Box::new(node(
18307                    31,
18308                    NodeKind::RangePat {
18309                        lo: Box::new(int_lit(32, "10")),
18310                        hi: Box::new(int_lit(33, "20")),
18311                        inclusive: true,
18312                    },
18313                )),
18314                guard: None,
18315                body: Box::new(block(34, vec![], Some(str_lit(35, "b")))),
18316            },
18317        );
18318        let else_arm = node(
18319            40,
18320            NodeKind::MatchArm {
18321                pattern: Box::new(node(41, NodeKind::WildcardPat)),
18322                guard: None,
18323                body: Box::new(block(42, vec![], Some(str_lit(43, "c")))),
18324            },
18325        );
18326        let f = return_match_fn("ClassifyRange", "n", "Int", vec![lo_arm, hi_arm, else_arm]);
18327        let out = gen(&module(vec![], vec![f]));
18328        assert!(
18329            !out.contains("case interface{}"),
18330            "range-pattern match must not emit a broken `case interface{{}}`, got: {out}"
18331        );
18332        // Exclusive `1..10` → `n >= 1 && n < 10`.
18333        assert!(
18334            out.contains("n >= 1") && out.contains("n < 10"),
18335            "`1..10` should test `n >= 1 && n < 10`, got: {out}"
18336        );
18337        // Inclusive `10..=20` → `n >= 10 && n <= 20`.
18338        assert!(
18339            out.contains("n >= 10") && out.contains("n <= 20"),
18340            "`10..=20` should test `n >= 10 && n <= 20`, got: {out}"
18341        );
18342    }
18343
18344    fn float_lit(id: u32, val: &str) -> AIRNode {
18345        node(
18346            id,
18347            NodeKind::Literal {
18348                lit: Literal::Float(val.into()),
18349            },
18350        )
18351    }
18352
18353    fn pow_fn(name: &str, ret_ty: &str, left: AIRNode, right: AIRNode) -> AIRNode {
18354        let pow = node(
18355            10,
18356            NodeKind::BinaryOp {
18357                op: BinOp::Pow,
18358                left: Box::new(left),
18359                right: Box::new(right),
18360            },
18361        );
18362        node(
18363            1,
18364            NodeKind::FnDecl {
18365                annotations: vec![],
18366                visibility: Visibility::Public,
18367                is_async: false,
18368                name: ident(name),
18369                generic_params: vec![],
18370                params: vec![],
18371                return_type: Some(Box::new(node(
18372                    2,
18373                    NodeKind::TypeNamed {
18374                        path: type_path(&[ret_ty]),
18375                        args: vec![],
18376                    },
18377                ))),
18378                effect_clause: vec![],
18379                where_clause: vec![],
18380                body: Box::new(block(3, vec![], Some(pow))),
18381            },
18382        )
18383    }
18384
18385    #[test]
18386    fn pow_int_lowers_to_int_pow_helper() {
18387        // `2 ** 10` (Int ** Int) must NOT emit the broken `(2 /* pow */ 10)`
18388        // (a Go syntax error) — it lowers to the `__bockIntPow` runtime helper
18389        // with both operands coerced to `int64`, and the helper is emitted.
18390        let f = pow_fn("p", "Int", int_lit(11, "2"), int_lit(12, "10"));
18391        let out = gen(&module(vec![], vec![f]));
18392        assert!(
18393            out.contains("__bockIntPow(int64(2), int64(10))"),
18394            "Int pow should lower to __bockIntPow, got: {out}"
18395        );
18396        assert!(
18397            !out.contains("/* pow */"),
18398            "Int pow must not emit the broken `/* pow */` form, got: {out}"
18399        );
18400        assert!(
18401            out.contains("func __bockIntPow("),
18402            "the integer-power runtime helper must be emitted, got: {out}"
18403        );
18404    }
18405
18406    #[test]
18407    fn pow_float_lowers_to_math_pow() {
18408        // `2.0 ** 3.0` (Float ** Float) lowers to `math.Pow` (float64 in/out)
18409        // and pulls in the `math` import.
18410        let f = pow_fn("p", "Float", float_lit(11, "2.0"), float_lit(12, "3.0"));
18411        let out = gen(&module(vec![], vec![f]));
18412        assert!(
18413            out.contains("math.Pow(float64(2.0), float64(3.0))"),
18414            "Float pow should lower to math.Pow, got: {out}"
18415        );
18416        assert!(
18417            out.contains("\"math\""),
18418            "Float pow must import the math package, got: {out}"
18419        );
18420        assert!(
18421            !out.contains("/* pow */"),
18422            "Float pow must not emit the broken `/* pow */` form, got: {out}"
18423        );
18424    }
18425
18426    fn match_arm(id: u32, pattern: AIRNode, body_str: &str) -> AIRNode {
18427        node(
18428            id,
18429            NodeKind::MatchArm {
18430                pattern: Box::new(pattern),
18431                guard: None,
18432                body: Box::new(block(id + 1, vec![], Some(str_lit(id + 2, body_str)))),
18433            },
18434        )
18435    }
18436
18437    fn constructor_pat(id: u32, name: &str, fields: Vec<AIRNode>) -> AIRNode {
18438        node(
18439            id,
18440            NodeKind::ConstructorPat {
18441                path: type_path(&[name]),
18442                fields,
18443            },
18444        )
18445    }
18446
18447    #[test]
18448    fn go_valpos_bind_match_routes_to_ifchain_and_binds() {
18449        // Q-go-valpos-bind-match: `return match n { x => "got ${x}" }`. A bare
18450        // bind has no value to switch on, so the value-switch IIFE emitted the
18451        // broken `case interface{}:` and dropped `x`. It must route to the
18452        // if-chain, binding `x := n` in an unconditional `else`.
18453        let arm = match_arm(20, bind_pat(21, "x"), "got it");
18454        let f = return_match_fn("EchoBinding", "n", "Int", vec![arm]);
18455        let out = gen(&module(vec![], vec![f]));
18456        assert!(
18457            !out.contains("case interface{}"),
18458            "bind-pattern match must not emit `case interface{{}}`, got: {out}"
18459        );
18460        assert!(
18461            out.contains("x := n"),
18462            "the bind arm must introduce `x := n`, got: {out}"
18463        );
18464    }
18465
18466    #[test]
18467    fn go_valpos_nested_optional_match_binds_nested_payload() {
18468        // Q-go-nested-optional-match: `match val { Some(Ok(n)) => …; Some(Err(e))
18469        // => …; None => … }`. The nested arm must route to the if-chain (the flat
18470        // tag-switch dropped the nested `n`), testing both `.tag`s and binding `n`
18471        // off the typed nested `.v` payload — never `undefined: n`.
18472        let some_ok = constructor_pat(
18473            20,
18474            "Some",
18475            vec![constructor_pat(21, "Ok", vec![bind_pat(22, "n")])],
18476        );
18477        let some_err = constructor_pat(
18478            30,
18479            "Some",
18480            vec![constructor_pat(31, "Err", vec![bind_pat(32, "e")])],
18481        );
18482        let none = constructor_pat(40, "None", vec![]);
18483        let arms = vec![
18484            match_arm(50, some_ok, "got n"),
18485            match_arm(53, some_err, "err e"),
18486            match_arm(56, none, "nothing"),
18487        ];
18488        let f = return_match_fn("NestedUnwrap", "val", "Optional", arms);
18489        let out = gen(&module(vec![], vec![f]));
18490        assert!(
18491            out.contains("val.tag == \"Some\"") && out.contains(".tag == \"Ok\""),
18492            "nested Optional/Result match must test both tags, got: {out}"
18493        );
18494        assert!(
18495            out.contains("n := "),
18496            "the nested `Ok(n)` payload must be bound, got: {out}"
18497        );
18498        assert!(
18499            !out.contains("case interface{}"),
18500            "nested match must not emit a broken `case interface{{}}`, got: {out}"
18501        );
18502    }
18503
18504    #[test]
18505    fn go_valpos_plain_record_match_routes_to_ifchain_and_binds() {
18506        // Q-plainrecord-valpos-match: `match p { Point { x, .. } => "x=${x}" }`. A
18507        // plain record is a concrete struct (no sealed-interface type/value to
18508        // switch on), so the value/type-switch emitted the broken `case Point:`
18509        // and dropped `x`. It must route to the if-chain, reading `x := p.X`.
18510        let rec_pat = node(
18511            20,
18512            NodeKind::RecordPat {
18513                path: type_path(&["Point"]),
18514                fields: vec![bock_air::AirRecordPatternField {
18515                    name: ident("x"),
18516                    pattern: None,
18517                }],
18518                rest: true,
18519            },
18520        );
18521        let arm = match_arm(30, rec_pat, "x val");
18522        let f = return_match_fn("GetX", "p", "Point", vec![arm]);
18523        let out = gen(&module(vec![], vec![f]));
18524        assert!(
18525            !out.contains("case Point") && !out.contains("case interface{}"),
18526            "plain-record match must not emit `case Point` / `case interface{{}}`, got: {out}"
18527        );
18528        assert!(
18529            out.contains("x := p.X"),
18530            "the plain-record field must be bound as `x := p.X`, got: {out}"
18531        );
18532    }
18533
18534    /// A `Fn(...) -> Void` *parameter* type lowers to a Go `func(...)` with NO
18535    /// result type — `func() struct{}` (the Void *value* type as a result) is a
18536    /// function that must `return struct{}{}`, which a void closure body never
18537    /// does, so the closure would not satisfy the parameter. Guards Item 1's
18538    /// type-lowering fix (`type_to_go`'s `TypeFunction` arm).
18539    #[test]
18540    fn fn_void_param_type_lowers_to_bare_func() {
18541        // fn on_click(handler: Fn() -> Void) -> Void { handler() }
18542        let fn_void_ty = node(
18543            2,
18544            NodeKind::TypeFunction {
18545                params: vec![],
18546                ret: Box::new(type_named_node(3, "Void")),
18547                effects: vec![],
18548            },
18549        );
18550        let handler_param = node(
18551            4,
18552            NodeKind::Param {
18553                pattern: Box::new(bind_pat(5, "handler")),
18554                ty: Some(Box::new(fn_void_ty)),
18555                default: None,
18556            },
18557        );
18558        let call = node(
18559            6,
18560            NodeKind::Call {
18561                callee: Box::new(id_node(7, "handler")),
18562                args: vec![],
18563                type_args: vec![],
18564            },
18565        );
18566        let f = node(
18567            1,
18568            NodeKind::FnDecl {
18569                annotations: vec![],
18570                visibility: Visibility::Public,
18571                is_async: false,
18572                name: ident("on_click"),
18573                generic_params: vec![],
18574                params: vec![handler_param],
18575                return_type: Some(Box::new(type_named_node(8, "Void"))),
18576                effect_clause: vec![],
18577                where_clause: vec![],
18578                body: Box::new(block(9, vec![call], None)),
18579            },
18580        );
18581        let out = gen(&module(vec![], vec![f]));
18582        assert!(
18583            out.contains("handler func()"),
18584            "`Fn() -> Void` param should lower to `func()`, got: {out}"
18585        );
18586        assert!(
18587            !out.contains("func() struct{}"),
18588            "`Fn() -> Void` must NOT emit `func() struct{{}}`, got: {out}"
18589        );
18590    }
18591
18592    /// An inherent (`impl Type`) method whose name a `trait` the type implements
18593    /// also declares is exported (`Render`, not `render`) so it satisfies the Go
18594    /// interface directly, AND the redundant same-named `impl Trait for Type`
18595    /// forwarder (`fn render(self) { self.render() }`) is skipped — emitting it
18596    /// would produce `func (T) Render() { return self.Render() }`, infinite
18597    /// recursion. Guards Item 2 (method-name casing + no self-recursive
18598    /// forwarder).
18599    #[test]
18600    fn inherent_method_exported_for_trait_and_no_recursive_forwarder() {
18601        // trait Component { fn render(self) -> String }
18602        let trait_method = node(
18603            10,
18604            NodeKind::FnDecl {
18605                annotations: vec![],
18606                visibility: Visibility::Public,
18607                is_async: false,
18608                name: ident("render"),
18609                generic_params: vec![],
18610                params: vec![param_node(11, "self")],
18611                return_type: Some(Box::new(type_named_node(12, "String"))),
18612                effect_clause: vec![],
18613                where_clause: vec![],
18614                body: Box::new(block(13, vec![], None)),
18615            },
18616        );
18617        let trait_decl = node(
18618            14,
18619            NodeKind::TraitDecl {
18620                annotations: vec![],
18621                visibility: Visibility::Public,
18622                is_platform: false,
18623                name: ident("Component"),
18624                generic_params: vec![],
18625                associated_types: vec![],
18626                methods: vec![trait_method],
18627            },
18628        );
18629        // impl Button { fn render(self) -> String { "x" } }  (private, inherent)
18630        let inherent_method = node(
18631            20,
18632            NodeKind::FnDecl {
18633                annotations: vec![],
18634                visibility: Visibility::Private,
18635                is_async: false,
18636                name: ident("render"),
18637                generic_params: vec![],
18638                params: vec![param_node(21, "self")],
18639                return_type: Some(Box::new(type_named_node(22, "String"))),
18640                effect_clause: vec![],
18641                where_clause: vec![],
18642                body: Box::new(block(23, vec![], Some(str_lit(24, "x")))),
18643            },
18644        );
18645        let inherent_impl = node(
18646            25,
18647            NodeKind::ImplBlock {
18648                annotations: vec![],
18649                generic_params: vec![],
18650                trait_path: None,
18651                trait_args: vec![],
18652                target: Box::new(type_named_node(26, "Button")),
18653                where_clause: vec![],
18654                methods: vec![inherent_method],
18655            },
18656        );
18657        // impl Component for Button { fn render(self) -> String { self.render() } }
18658        let forwarder_method = node(
18659            30,
18660            NodeKind::FnDecl {
18661                annotations: vec![],
18662                visibility: Visibility::Public,
18663                is_async: false,
18664                name: ident("render"),
18665                generic_params: vec![],
18666                params: vec![param_node(31, "self")],
18667                return_type: Some(Box::new(type_named_node(32, "String"))),
18668                effect_clause: vec![],
18669                where_clause: vec![],
18670                body: Box::new(block(
18671                    33,
18672                    vec![],
18673                    Some(node(
18674                        34,
18675                        NodeKind::MethodCall {
18676                            receiver: Box::new(id_node(35, "self")),
18677                            method: ident("render"),
18678                            type_args: vec![],
18679                            args: vec![],
18680                        },
18681                    )),
18682                )),
18683            },
18684        );
18685        let trait_impl = node(
18686            36,
18687            NodeKind::ImplBlock {
18688                annotations: vec![],
18689                generic_params: vec![],
18690                trait_path: Some(type_path(&["Component"])),
18691                trait_args: vec![],
18692                target: Box::new(type_named_node(37, "Button")),
18693                where_clause: vec![],
18694                methods: vec![forwarder_method],
18695            },
18696        );
18697        let out = gen(&module(vec![], vec![trait_decl, inherent_impl, trait_impl]));
18698        // The inherent method is exported to `Render`.
18699        assert!(
18700            out.contains("Button) Render() string"),
18701            "inherent method should be exported `Render`, got: {out}"
18702        );
18703        // The lowercase inherent name must not survive.
18704        assert!(
18705            !out.contains("Button) render() string"),
18706            "inherent method must not stay lowercase `render`, got: {out}"
18707        );
18708        // The self-recursive forwarder must be skipped (only ONE `Render` body).
18709        assert_eq!(
18710            out.matches("Button) Render()").count(),
18711            1,
18712            "exactly one `Render` method should be emitted on Button, got: {out}"
18713        );
18714        assert!(
18715            !out.contains("return self.Render()"),
18716            "the self-recursive forwarder must be eliminated, got: {out}"
18717        );
18718    }
18719}