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 } = ¶m.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(_)) => ¶ms[1..],
7264 _ => ¶ms[..],
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(), ¶ms[1..]),
7891 _ => (
7892 receiver_type
7893 .chars()
7894 .next()
7895 .unwrap_or('r')
7896 .to_lowercase()
7897 .to_string(),
7898 ¶ms[..],
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, ¶m_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}