Skip to main content

bock_codegen/
rs.rs

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