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, 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/// Prelude container value/type names the Rust backend lowers to **native**
32/// Rust (`Optional`/`Result` → `Option`/`Result`; `Some`/`None`/`Ok`/`Err` are
33/// native constructors) rather than to a cross-module import. The per-module
34/// `use`-emission pass skips these: they are not real exports of the declaring
35/// stdlib module, so a `use crate::core::option::Some;` would not resolve. The
36/// comparison `Ordering` enum is deliberately **absent** — `core.compare`
37/// genuinely declares (`public enum Ordering`) and exports it, so a cross-module
38/// use of it resolves through a real `use crate::core::compare::Ordering;`.
39const RS_NATIVE_PRELUDE_NAMES: &[&str] = &["Optional", "Result", "Some", "None", "Ok", "Err"];
40
41/// Conservative module scan for `Channel` / `spawn` references.
42fn rs_module_uses_concurrency(items: &[AIRNode]) -> bool {
43    items.iter().any(|n| {
44        let s = format!("{n:?}");
45        s.contains("\"Channel\"") || s.contains("\"spawn\"")
46    })
47}
48
49/// Runtime helpers for Bock concurrency in Rust. Backed by
50/// `tokio::sync::mpsc::unbounded_channel`.
51const CONCURRENCY_RUNTIME_RS: &str = "\
52// ── Bock concurrency runtime ──
53use std::sync::Arc;
54pub struct __BockChannel<T> {
55    tx: tokio::sync::mpsc::UnboundedSender<T>,
56    rx: tokio::sync::Mutex<tokio::sync::mpsc::UnboundedReceiver<T>>,
57}
58pub fn __bock_channel_new<T: Send + 'static>() -> (Arc<__BockChannel<T>>, Arc<__BockChannel<T>>) {
59    let (tx, rx) = tokio::sync::mpsc::unbounded_channel();
60    let ch = Arc::new(__BockChannel { tx, rx: tokio::sync::Mutex::new(rx) });
61    (ch.clone(), ch)
62}
63impl<T> __BockChannel<T> {
64    pub fn send(&self, v: T) { let _ = self.tx.send(v); }
65    pub async fn recv(&self) -> T {
66        let mut guard = self.rx.lock().await;
67        guard.recv().await.expect(\"channel closed\")
68    }
69    pub fn close(&self) {}
70}
71pub fn __bock_spawn<T: Send + 'static>(f: impl std::future::Future<Output = T> + Send + 'static) -> tokio::task::JoinHandle<T> {
72    tokio::spawn(f)
73}
74";
75
76/// Rust code generator implementing the `CodeGenerator` trait.
77#[derive(Debug)]
78pub struct RsGenerator {
79    profile: TargetProfile,
80}
81
82impl RsGenerator {
83    /// Creates a new Rust code generator.
84    #[must_use]
85    pub fn new() -> Self {
86        Self {
87            profile: TargetProfile::rust(),
88        }
89    }
90}
91
92impl Default for RsGenerator {
93    fn default() -> Self {
94        Self::new()
95    }
96}
97
98impl CodeGenerator for RsGenerator {
99    fn target(&self) -> &TargetProfile {
100        &self.profile
101    }
102
103    fn generate_module(&self, module: &AIRModule) -> Result<GeneratedCode, CodegenError> {
104        // Shared pre-pass: hoist value-position diverging control flow (see
105        // `hoist_value_cf`) into declare-then-assign temp blocks.
106        let module =
107            &crate::generator::hoist_value_cf(crate::generator::lower_blanket_into(module.clone()));
108        let mut ctx = RsEmitCtx::new();
109        ctx.enum_variants =
110            crate::generator::collect_enum_variants(&[(module, std::path::Path::new(""))]);
111        ctx.generic_decls =
112            crate::generator::collect_generic_decls(&[(module, std::path::Path::new(""))]);
113        ctx.collect_clone_targets(module);
114        ctx.collect_user_equatable_types(module);
115        // Aliases first: `collect_fn_returning_fns` resolves a `Fn`-typed alias
116        // in a return position, so the alias table must already be populated.
117        ctx.collect_fn_type_aliases(module);
118        ctx.collect_fn_returning_fns(module);
119        let trait_decls =
120            crate::generator::collect_trait_decls(&[(module, std::path::Path::new(""))]);
121        ctx.collect_self_operand_methods(&trait_decls);
122        ctx.trait_decls = trait_decls;
123        ctx.emit_node(module)?;
124        let content = ctx.finish();
125        let source_map = SourceMap {
126            generated_file: String::new(),
127            ..Default::default()
128        };
129        Ok(GeneratedCode {
130            files: vec![OutputFile {
131                path: PathBuf::new(),
132                content,
133                source_map: Some(source_map),
134            }],
135        })
136    }
137
138    /// Emit a per-module **native Rust module tree** (spec §20.6.1; DQ19
139    /// resolved): each module the entry program reaches through a real `use` is
140    /// emitted to its **own** `.rs` file under `src/`, wired with Rust's native
141    /// module system (`mod <m>;` declarations + `use crate::<m>::<x>;` for
142    /// cross-module references). This is the sole `bock build` output path.
143    ///
144    /// ## Layout (cargo-idiomatic `src/`-rooted crate)
145    ///
146    /// Codegen emits the `src/`-rooted source tree (in all modes); in project
147    /// mode the scaffolder adds the `Cargo.toml` run affordance (S6a / DV18), so
148    /// `build/rust/` becomes a runnable Cargo crate:
149    /// - `Cargo.toml` — minimal manifest (`[package]` + a `[[bin]]` at
150    ///   `src/main.rs`), emitted by the **scaffolder** in project mode only —
151    ///   just enough to `cargo run`.
152    /// - `src/main.rs` — the entry module's body, preceded by `mod <seg>;`
153    ///   declarations for every top-level namespace the tree contains.
154    /// - `src/<path>.rs` — one file per reached non-entry module, mirrored from
155    ///   its **declared** module-path (`module core.option` ⇒ `src/core/option.rs`).
156    /// - `src/<namespace>.rs` — a wiring file per intermediate namespace
157    ///   (`src/core.rs` declaring `pub mod option; pub mod iter; …`), since Rust
158    ///   requires every file be reached through a `mod` declaration.
159    ///
160    /// §20.6.1 allows "the target ecosystem's conventions," so the `src/`-rooted
161    /// mirror is the correct idiomatic layout. The crate is run via `cargo run`
162    /// from `build/rust/` (debug build — see the rust run plan in
163    /// `bock-build`'s `toolchain.rs`).
164    ///
165    /// ## Cross-module references
166    ///
167    /// Each emitted file lists its cross-module dependencies as
168    /// `use crate::<declared::path>::<symbol>;` at the top — both the explicit
169    /// `use`d symbols and the implicit §18.2-prelude names a module references
170    /// but does not `use` (e.g. a base trait in an `impl`). The symbols are then
171    /// referenced unqualified in the body exactly as the bundling path emitted
172    /// them, so the per-item lowering is unchanged.
173    ///
174    /// The concurrency runtime (used by `Channel`/`spawn` programs) is emitted
175    /// **once** into a shared `src/bock_runtime.rs`; modules referencing it
176    /// `use crate::bock_runtime::*;`. Rust uses a native `fn main`, so no entry
177    /// invocation is appended.
178    fn generate_project(
179        &self,
180        modules: &[(&AIRModule, &std::path::Path)],
181    ) -> Result<GeneratedCode, CodegenError> {
182        // Shared pre-pass: hoist value-position diverging control flow on every
183        // module before registry collection or emission (see `hoist_value_cf`).
184        let hoisted: Vec<(AIRModule, &std::path::Path)> = modules
185            .iter()
186            .map(|(m, p)| {
187                (
188                    crate::generator::hoist_value_cf(crate::generator::lower_blanket_into(
189                        (*m).clone(),
190                    )),
191                    *p,
192                )
193            })
194            .collect();
195        let modules: Vec<(&AIRModule, &std::path::Path)> =
196            hoisted.iter().map(|(m, p)| (m, *p)).collect();
197        let modules = modules.as_slice();
198        // Emit only modules the entry program actually `use`s (plus the entry
199        // itself), dependency-ordered — never the prelude-only stdlib.
200        let reachable = crate::generator::reachable_modules(modules);
201        let modules = reachable.as_slice();
202        if modules.is_empty() {
203            return Ok(GeneratedCode { files: vec![] });
204        }
205
206        let entry_idx = modules
207            .iter()
208            .position(|(m, _)| crate::generator::module_declares_main_fn(m))
209            .unwrap_or(modules.len() - 1);
210
211        // Registries collected across the whole reachable set so a reference in
212        // one file to a type/variant/trait declared in another lowers
213        // identically to the bundling path.
214        let enum_variants = crate::generator::collect_enum_variants(modules);
215        let generic_decls = crate::generator::collect_generic_decls(modules);
216        let trait_decls = crate::generator::collect_trait_decls(modules);
217        let public_symbols = crate::generator::collect_public_symbol_modules(modules);
218
219        // Map each trait's method names → (declaring module-path, trait name).
220        // A cross-module call of a trait method (`x.message()` where `Error` is
221        // declared in `core.error`) requires the *trait* to be in scope in Rust
222        // (`use crate::core::error::Error;`) — the bundling path had it for free
223        // (one crate root). The implicit-import scan only sees the *method* name
224        // (`message`) referenced, not the trait, so this lets the per-module
225        // emitter import the trait when its method is used. Built from the trait
226        // registry + the public-symbol map (which carries the declaring module).
227        let mut trait_method_owner: HashMap<String, (String, String)> = HashMap::new();
228        for (trait_name, info) in &trait_decls {
229            let Some(module_path) = public_symbols.get(trait_name) else {
230                continue; // a non-public / local-only trait needs no cross-module import
231            };
232            for m in &info.methods {
233                if let NodeKind::FnDecl { name, .. } = &m.kind {
234                    trait_method_owner
235                        .insert(name.name.clone(), (module_path.clone(), trait_name.clone()));
236                }
237            }
238        }
239
240        // Map each enum *variant* name → (declaring module-path, enum name). The
241        // Rust backend qualifies a variant as `Enum::Variant`, so a module that
242        // *constructs or matches* a cross-module enum's variant needs the **enum
243        // type** in scope (`use crate::core::compare::Ordering;`) — but the AIR
244        // it references names the *variant* (`Greater`), not the enum, so the
245        // plain implicit-import scan (which keys on the public *enum* name)
246        // misses it. This drives that import from a referenced variant. Built
247        // from the cross-module variant registry + the public-symbol map (which
248        // carries the enum's declaring module). Built-in Optional/Result/Ordering
249        // pre-seeds in the registry whose enum is not a real public symbol are
250        // skipped (they lower natively, not through a `use`).
251        let mut variant_enum_owner: HashMap<String, (String, String)> = HashMap::new();
252        for (variant, info) in &enum_variants {
253            if let Some(module_path) = public_symbols.get(&info.enum_name) {
254                variant_enum_owner.insert(
255                    variant.clone(),
256                    (module_path.clone(), info.enum_name.clone()),
257                );
258            }
259        }
260
261        // Self-operand + clone-target sets are global to the program (a generic
262        // helper in one module may take a clone-bound record from another), so
263        // collect them once into a template ctx and clone into each per-module
264        // ctx below.
265        let mut template = RsEmitCtx::new();
266        template.enum_variants = enum_variants;
267        template.generic_decls = generic_decls;
268        template.collect_self_operand_methods(&trait_decls);
269        template.trait_decls = trait_decls;
270        // Collect `Fn`-typed aliases across the whole reachable set first: a
271        // function in one module may return an alias declared in another, and
272        // `collect_fn_returning_fns` resolves through the alias table.
273        for (module, _) in modules {
274            template.collect_fn_type_aliases(module);
275        }
276        for (module, _) in modules {
277            template.collect_clone_targets(module);
278            template.collect_user_equatable_types(module);
279            template.collect_fn_returning_fns(module);
280        }
281        // Effect-op resolution needs the whole reachable set: a bare op in one
282        // module may belong to an effect declared in another (cross-module
283        // effects, §10 + DV13).
284        template.seed_effect_registries(modules);
285
286        // The non-entry reached module-paths, for the `mod`-tree wiring.
287        let mut tree_paths: Vec<String> = Vec::new();
288        let mut needs_runtime = false;
289
290        let mut files: Vec<OutputFile> = Vec::with_capacity(modules.len() + 3);
291        for (i, (module, source_path)) in modules.iter().enumerate() {
292            let own_path = crate::generator::module_path_string(module).unwrap_or_default();
293            let mut ctx = template.fork();
294            ctx.per_module = true;
295            let mut imports =
296                crate::generator::implicit_imports_for(module, &public_symbols, &own_path);
297            // Also import a cross-module trait whose *method* this module calls
298            // (`x.message()` ⇒ `use crate::core::error::Error;`), so the trait is
299            // in scope for method resolution. Conservative: a structural scan for
300            // the method name as a quoted identifier; a dead `use` is harmless
301            // (`#![allow(unused_imports)]`).
302            let rendered = format!("{module:?}");
303            for (method, (trait_module, trait_name)) in &trait_method_owner {
304                if trait_module == &own_path {
305                    continue; // trait declared locally — already in scope
306                }
307                if rendered.contains(&format!("\"{method}\"")) {
308                    imports.push((trait_module.clone(), trait_name.clone()));
309                }
310            }
311            // And import the *enum type* whose cross-module *variant* this module
312            // constructs/matches (`Ordering::Greater` ⇒ `use
313            // crate::core::compare::Ordering;`) — the AIR names the variant, but
314            // Rust qualifies it through the enum, which must be in scope.
315            for (variant, (enum_module, enum_name)) in &variant_enum_owner {
316                if enum_module == &own_path {
317                    continue; // enum declared locally — already in scope
318                }
319                if rendered.contains(&format!("\"{variant}\"")) {
320                    imports.push((enum_module.clone(), enum_name.clone()));
321                }
322            }
323            ctx.implicit_imports = imports;
324            ctx.emit_node(module)?;
325            needs_runtime |= ctx.concurrency_runtime_emitted;
326            let body = ctx.finish_per_module();
327
328            // The entry module's body is `src/main.rs` (preceded by the
329            // `mod`-tree declarations, prepended below); every other module is
330            // placed under `src/` at its declared-path mirror.
331            let rel = if i == entry_idx {
332                PathBuf::from("main.rs")
333            } else {
334                tree_paths.push(own_path.clone());
335                crate::generator::module_tree_relpath(module, source_path, self.target())
336            };
337            let out_path = PathBuf::from("src").join(&rel);
338            let generated_file = out_path
339                .file_name()
340                .and_then(|s| s.to_str())
341                .unwrap_or("")
342                .to_string();
343            files.push(OutputFile {
344                path: out_path,
345                content: body,
346                source_map: Some(SourceMap {
347                    generated_file,
348                    ..Default::default()
349                }),
350            });
351        }
352
353        // Build the `mod`-tree: `main.rs` gets `mod <top>;` for each top-level
354        // namespace; each intermediate namespace gets a `src/<ns>.rs` wiring
355        // file declaring `pub mod <child>;`. Add the shared runtime module too.
356        let mut tree = ModTree::default();
357        for p in &tree_paths {
358            tree.insert(p);
359        }
360        if needs_runtime {
361            tree.insert("bock_runtime");
362        }
363        let root_mods = tree.root_decls();
364        for (wiring_rel, decls) in tree.wiring_files() {
365            files.push(OutputFile {
366                path: PathBuf::from("src").join(&wiring_rel),
367                content: decls,
368                source_map: None,
369            });
370        }
371
372        // Insert the root `mod` declarations into `src/main.rs` *after* the
373        // leading `#![allow(...)]` inner attribute (an inner attribute must
374        // precede every item, so the `mod`s cannot go before it). They are
375        // placed at the top of the item region, ahead of the cross-module `use`s
376        // and the body.
377        if !root_mods.is_empty() {
378            if let Some(main_file) = files
379                .iter_mut()
380                .find(|f| f.path == PathBuf::from("src").join("main.rs"))
381            {
382                let block = format!("{root_mods}\n");
383                // The inner-attribute prefix ends at the first blank line after
384                // the `#![allow(...)]` line; insert the `mod`s right there. If
385                // (defensively) no inner attribute is present, prepend.
386                match main_file.content.find("]\n\n") {
387                    Some(idx) => {
388                        let at = idx + "]\n\n".len();
389                        main_file.content.insert_str(at, &block);
390                    }
391                    None => main_file.content.insert_str(0, &block),
392                }
393            }
394        }
395
396        // Shared concurrency runtime module (tokio-backed), emitted once.
397        if needs_runtime {
398            files.push(OutputFile {
399                path: PathBuf::from("src").join("bock_runtime.rs"),
400                content: format!("#![allow(unused_imports, dead_code)]\n{CONCURRENCY_RUNTIME_RS}"),
401                source_map: None,
402            });
403        }
404
405        // Manifest emission moved to the project-mode scaffolder (S6a / DV18):
406        // codegen emits only the per-module *source* tree in all modes; the
407        // `Cargo.toml` run affordance is emitted by `RustScaffolder` in project
408        // mode only (never under `--source-only`). See `scaffold.rs`.
409
410        Ok(GeneratedCode { files })
411    }
412
413    /// Transpile `@test` functions into an inline `#[cfg(test)] mod` (S7).
414    ///
415    /// `cargo test` runs the bin crate's inline test module. Each Bock `@test`
416    /// becomes a `#[test] fn`, with `expect(actual).<assertion>(expected)` chains
417    /// lowered to `assert!` / `assert_eq!`. The module is emitted to
418    /// `src/bock_tests.rs` and wired into `src/main.rs` via the returned
419    /// `entry_append` (`#[cfg(test)] mod bock_tests;`). `framework` is ignored:
420    /// `cargo test` is the universal Rust framework (§20.6.2).
421    fn generate_tests(
422        &self,
423        modules: &[(&AIRModule, &std::path::Path)],
424        _framework: &str,
425    ) -> Result<crate::generator::TestArtifacts, CodegenError> {
426        let reachable = crate::generator::reachable_modules(modules);
427        let modules = reachable.as_slice();
428        let tests = crate::generator::collect_test_fns(modules);
429        if tests.is_empty() {
430            return Ok(crate::generator::TestArtifacts::default());
431        }
432
433        // Build the same cross-module registries `generate_project` uses so the
434        // test bodies lower references (enum variants, generics, trait methods)
435        // identically to the runtime tree.
436        let enum_variants = crate::generator::collect_enum_variants(modules);
437        let generic_decls = crate::generator::collect_generic_decls(modules);
438        let trait_decls = crate::generator::collect_trait_decls(modules);
439        let mut template = RsEmitCtx::new();
440        template.enum_variants = enum_variants;
441        template.generic_decls = generic_decls;
442        template.collect_self_operand_methods(&trait_decls);
443        template.trait_decls = trait_decls;
444        // Aliases first (see the project path): the alias table must be complete
445        // before `collect_fn_returning_fns` resolves a `Fn`-typed alias return.
446        for (module, _) in modules {
447            template.collect_fn_type_aliases(module);
448        }
449        for (module, _) in modules {
450            template.collect_clone_targets(module);
451            template.collect_user_equatable_types(module);
452            template.collect_fn_returning_fns(module);
453        }
454        template.seed_effect_registries(modules);
455
456        // The test module lives at the crate root; `use super::*` brings in
457        // everything the bin's `main.rs` (the entry module) declares. Each
458        // *non-entry* reachable module is a real `crate::<ns>` submodule, so
459        // bring each top-level namespace in too, letting a test call functions
460        // declared in a `use`d module. (The entry module is the crate root, not
461        // a `crate::<entry>` submodule, so it must NOT be added here.)
462        let entry_idx = modules
463            .iter()
464            .position(|(m, _)| crate::generator::module_declares_main_fn(m))
465            .unwrap_or(modules.len() - 1);
466        let mut ctx = template.fork();
467        ctx.per_module = true;
468        ctx.indent = 0;
469        ctx.writeln("use super::*;");
470        let mut namespaces: std::collections::BTreeSet<String> = std::collections::BTreeSet::new();
471        for (i, (module, _)) in modules.iter().enumerate() {
472            if i == entry_idx {
473                continue;
474            }
475            if let Some(p) = crate::generator::module_path_string(module) {
476                if let Some(top) = p.split('.').next() {
477                    if !top.is_empty() {
478                        namespaces.insert(top.to_string());
479                    }
480                }
481            }
482        }
483        for ns in &namespaces {
484            ctx.writeln(&format!("use crate::{ns}::*;"));
485        }
486
487        for (test_fn, _module_path) in &tests {
488            let NodeKind::FnDecl { name, body, .. } = &test_fn.kind else {
489                continue;
490            };
491            ctx.buf.push('\n');
492            ctx.writeln("#[test]");
493            ctx.writeln(&format!("fn {}() {{", to_snake_case(&name.name)));
494            ctx.indent += 1;
495            ctx.emit_test_body(body)?;
496            ctx.indent -= 1;
497            ctx.writeln("}");
498        }
499
500        // `src/bock_tests.rs` IS the `bock_tests` module body (it is reached via
501        // `#[cfg(test)] mod bock_tests;` in `main.rs`), so the `use`/`#[test] fn`
502        // items go at file top with NO extra `mod` wrapper — otherwise `super`
503        // would resolve to a spurious inner module instead of the crate root and
504        // `use super::*` would not bring in the bin's items. The whole file is
505        // already `#[cfg(test)]`-gated by the `mod bock_tests;` declaration.
506        let body = ctx.buf;
507        let content = format!("#![allow(unused_imports, unused_parens, dead_code)]\n\n{body}");
508
509        Ok(crate::generator::TestArtifacts {
510            files: vec![OutputFile {
511                path: PathBuf::from("src").join("bock_tests.rs"),
512                content,
513                source_map: None,
514            }],
515            entry_append: Some("\n#[cfg(test)]\nmod bock_tests;\n".to_string()),
516        })
517    }
518}
519
520/// Builder for the Rust `mod` declaration tree of a per-module crate.
521///
522/// Rust requires every source file be reached through a `mod`/`pub mod`
523/// declaration from the crate root. Given the set of reached non-entry
524/// module-paths (`core.option`, `helper`, `bock_runtime`, …), this produces:
525/// - the root declarations for `src/main.rs` (`mod core;`, `mod helper;`, … —
526///   one per distinct top-level namespace), and
527/// - one wiring file per intermediate namespace (`src/core.rs` declaring
528///   `pub mod option;`, `pub mod iter;`, …).
529///
530/// All v1 modules are leaves under a namespace (`core.X`) or bare roots
531/// (`helper`), but the builder handles an arbitrarily deep tree.
532#[derive(Default)]
533struct ModTree {
534    /// Child namespaces keyed by dotted prefix. The empty string is the crate
535    /// root; `core` maps to the children declared in `src/core.rs`. Values are
536    /// the immediate child segment names (deduped, sorted on render).
537    children: std::collections::BTreeMap<String, std::collections::BTreeSet<String>>,
538}
539
540impl ModTree {
541    /// Register a reached module-path, recording every parent→child edge so the
542    /// crate root and each intermediate namespace declare the right submodules.
543    fn insert(&mut self, dotted: &str) {
544        let segs: Vec<&str> = dotted.split('.').filter(|s| !s.is_empty()).collect();
545        let mut prefix = String::new();
546        for seg in &segs {
547            self.children
548                .entry(prefix.clone())
549                .or_default()
550                .insert((*seg).to_string());
551            if prefix.is_empty() {
552                prefix = (*seg).to_string();
553            } else {
554                prefix.push('.');
555                prefix.push_str(seg);
556            }
557        }
558    }
559
560    /// The crate-root `mod <seg>;` declarations (for `src/main.rs`), one per
561    /// distinct top-level namespace, newline-terminated. Empty when the program
562    /// has no cross-module dependencies.
563    fn root_decls(&self) -> String {
564        let mut out = String::new();
565        if let Some(roots) = self.children.get("") {
566            for seg in roots {
567                out.push_str(&format!("mod {seg};\n"));
568            }
569        }
570        out
571    }
572
573    /// One wiring file per intermediate namespace: `(relative path, contents)`
574    /// where the path is `<namespace>.rs` (e.g. `core.rs`) and the contents are
575    /// its `pub mod <child>;` declarations. Excludes the crate root (whose decls
576    /// go in `main.rs` via [`Self::root_decls`]).
577    fn wiring_files(&self) -> Vec<(PathBuf, String)> {
578        let mut files = Vec::new();
579        for (prefix, kids) in &self.children {
580            if prefix.is_empty() {
581                continue;
582            }
583            let mut content = String::new();
584            for kid in kids {
585                content.push_str(&format!("pub mod {kid};\n"));
586            }
587            let rel: PathBuf = prefix.split('.').collect::<PathBuf>().with_extension("rs");
588            files.push((rel, content));
589        }
590        files
591    }
592}
593
594// ─── Emission context ────────────────────────────────────────────────────────
595
596/// Internal state for Rust emission.
597struct RsEmitCtx {
598    buf: String,
599    indent: usize,
600    /// Track whether we need `use std::rc::Rc;` at the top.
601    needs_rc_import: bool,
602    /// Track whether we need `use std::sync::Arc;` at the top.
603    needs_arc_import: bool,
604    /// Names bound in the current block whose Call value is wrapped in
605    /// `tokio::spawn(...)` because the binding is later `await`ed within the
606    /// same block. Rust futures are lazy, so without this, sequential
607    /// `.await` on each binding would serialise the work. See
608    /// [`Self::collect_task_bindings`].
609    task_bound_names: std::collections::HashSet<String>,
610    /// Maps effect operation name → effect type name (e.g., "log" → "Logger").
611    effect_ops: HashMap<String, String>,
612    /// Maps effect type name → current handler variable name in scope.
613    current_handler_vars: HashMap<String, String>,
614    /// Effect type names whose in-scope handler variable is *already a reference*
615    /// (`&impl Effect`) — i.e. an effectful function's own `&impl Effect`
616    /// parameter forwarded to a nested effectful call. Forwarding such a handler
617    /// must pass it *as-is* (`handler`), not re-borrowed (`&handler`), which would
618    /// be `&&impl Effect` and fail the `Effect` trait bound (`E0277`). A handler
619    /// that is a concrete owned value instead (module-level `handle` const, a
620    /// `handling`-block local) is NOT in this set and is forwarded as `&handler`.
621    /// Saved/restored alongside [`Self::current_handler_vars`] at every scope that
622    /// rebinds handlers.
623    borrowed_handler_effects: std::collections::HashSet<String>,
624    /// Maps function name → effect type names from its `with` clause.
625    fn_effects: HashMap<String, Vec<String>>,
626    /// Maps composite effect name → component effect names.
627    composite_effects: HashMap<String, Vec<String>>,
628    /// Set once the concurrency runtime prelude has been emitted in the
629    /// single-module self-contained path ([`RustGenerator::generate_module`]), so
630    /// a module referencing it more than once still inlines it at most once (a
631    /// duplicate `struct __BockChannel` is a Rust redefinition error). The
632    /// per-module project path emits the runtime once into a shared module.
633    concurrency_runtime_emitted: bool,
634    /// User-enum-variant registry (DV14). Maps a variant name to its enum so a
635    /// construction (`Circle { .. }`, `Rect(..)`, `Empty`) and a match pattern
636    /// can be qualified `Enum::Variant`, which Rust requires (an unqualified
637    /// variant does not resolve at the crate root). Pre-scanned across the
638    /// reached modules; consulted *after* the bespoke Optional/Result paths so
639    /// those are never regressed.
640    enum_variants: crate::generator::EnumVariantRegistry,
641    /// Generic-type declaration registry: a record/enum/class name → its
642    /// declared generic params. An `impl Box { ... }` block carries no params of
643    /// its own (the `T` is declared on `record Box[T]`); Rust requires the impl
644    /// to introduce and apply them (`impl<T> Box<T> { ... }`). This recovers them
645    /// at the impl site. Pre-scanned across the reached modules (mirrors
646    /// [`Self::enum_variants`]).
647    generic_decls: crate::generator::GenericDeclRegistry,
648    /// Records whose `impl` returns a `self` field by value and so need
649    /// `#[derive(Clone)]` plus a `T: Clone` bound on the generic impl (a `&self`
650    /// method cannot move a non-`Copy` field out, so the field read is lowered
651    /// to `self.field.clone()`). Populated by [`Self::collect_clone_targets`]
652    /// before emission so the `RecordDecl` can decide whether to derive `Clone`.
653    clone_target_records: std::collections::HashSet<String>,
654    /// Names of *generic* records whose inherent or trait `impl` will carry a
655    /// `T: Clone` bound — either because they return a `self` field by value
656    /// ([`Self::clone_target_records`]) or because a method clones a generic
657    /// collection element ([`Self::body_clones_collection_element`], e.g.
658    /// `ListIterator.next` doing `self.xs.get(self.cursor)`). A free generic
659    /// function that takes such a record by value and calls a method on it
660    /// (`count[T](it: ListIterator[T])` driving `it.next()`) must propagate the
661    /// bound, or method resolution fails (`E0599`: trait bounds not satisfied).
662    /// Populated by [`Self::collect_clone_targets`].
663    clone_bound_records: std::collections::HashSet<String>,
664    /// Names of record/enum/class types that carry an explicit `impl Equatable`
665    /// (the checker's [`bock_types::checker::CUSTOM_EQ_META_KEY`] stamp). Such a
666    /// type is given BOTH an `impl Equatable` (its `eq`) AND a delegating
667    /// `impl PartialEq` (DQ31 — so it compares natively inside Rust containers;
668    /// see [`Self::emit_delegating_partial_eq`]), which makes a bare
669    /// `a.eq(&b)` ambiguous between `Equatable::eq` and `PartialEq::eq` (E0034).
670    /// A desugared `.eq` method call whose receiver is one of these types is
671    /// therefore emitted as the fully-qualified trait call
672    /// `Equatable::eq(&a, &b)`. Populated by
673    /// [`Self::collect_user_equatable_types`].
674    /// (Q-rust-equatable-eq-collision.)
675    user_equatable_types: std::collections::HashSet<String>,
676    /// True while emitting a method body whose impl target is generic and clones
677    /// `self` fields. Gates the `self.field` → `self.field.clone()` rewrite so it
678    /// applies only inside such methods (never to general field reads, which
679    /// would be noisy and could over-require `Clone`).
680    in_clone_self_method: bool,
681    /// True while emitting the **target** (LHS) of an assignment
682    /// (`self.cursor = …`). Suppresses the [`Self::in_clone_self_method`]
683    /// `self.field` → `self.field.clone()` rewrite there: an assignment target is
684    /// a place expression, and `self.cursor.clone() = …` is not valid Rust. Set
685    /// and cleared around the target emit in the `Assign` arm.
686    in_assign_target: bool,
687    /// Names of trait methods whose non-receiver operand is `Self`-typed
688    /// (`compare`/`eq`/`beats`/…). Such an operand is emitted and *called* by
689    /// shared reference in Rust: the trait/impl signature is `other: &Self` /
690    /// `other: &Target`, and a desugared call borrows the argument
691    /// (`a.compare(&b)`). Bock's value semantics permit reusing the argument
692    /// after the call (e.g. stdlib `max` does `match a.compare(b) { _ => b }`),
693    /// which by-value would move a non-`Copy` value out (Rust E0382). Derived
694    /// from the trait registry; keyed by the bare method name (globally unique
695    /// within a v1 program).
696    self_operand_methods: std::collections::HashSet<String>,
697    /// Names of match-pattern bindings in the current arm that are *used more
698    /// than once* in the arm body. Such a binding (`Some(x) => ... pred(x) ...
699    /// [x] ...`) is moved by its first by-value consumer (the Rust pattern
700    /// binds by value), so each later by-value use must clone to keep the value
701    /// live (`E0382`: use of moved value). When a bare-identifier call argument
702    /// names a binding in this set, codegen emits `x.clone()` rather than `x`.
703    /// The clone is always valid: a generic such binding is element-typed and
704    /// its fn already carries the matching `T: Clone` bound (e.g.
705    /// `filter[T](.., pred: Fn(T) -> Bool)`), and concrete v1 element types are
706    /// `Clone`. Saved/restored around each arm so it never leaks across arms.
707    reused_match_bindings: std::collections::HashSet<String>,
708    /// Snake-cased names of `let`-bound variables in the current block that are
709    /// read by-value more than once (a non-`Copy` value passed by value to a
710    /// free function is *moved* by the first consumer, so a later by-value pass
711    /// is `E0382`). A bare-identifier free-function argument naming such a
712    /// binding is emitted as `x.clone()`. Mirrors [`Self::reused_match_bindings`]
713    /// for `let` bindings rather than match-arm bindings: the same move-reuse
714    /// hazard arises whenever a query helper (`size(s)`, `contains(s, x)`,
715    /// `to_list(s)`) takes a record by value and the binding is queried again.
716    /// The clone is sound: a concrete v1 record/collection derives `Clone`, and a
717    /// generic such binding lives in a fn already carrying the matching `T:
718    /// Clone` bound. Seeded per-block (saved/restored) so it never leaks.
719    reused_let_bindings: std::collections::HashSet<String>,
720    /// The reached modules' user-declared traits (keyed by name). Used to
721    /// distinguish a
722    /// `T: Equatable` bound that is a real user trait (it has an `impl`, so the
723    /// bound and the `.eq` call dispatch normally) from the compiler-provided
724    /// sealed-core conformance, which must be lowered to the Rust std trait /
725    /// native operator (GAP-C). See [`crate::generator::is_unimplemented_sealed_core_trait`].
726    trait_decls: crate::generator::TraitDeclRegistry,
727    /// True in the **per-module native-module** emission path
728    /// ([`RustGenerator::generate_project`], the sole real-build path). When set,
729    /// the `Module` arm emits real `use crate::<m>::<x>;` for cross-module
730    /// references (explicit `use`s and the implicit prelude imports) at the top
731    /// of the file instead of dropping the `ImportDecl`s, and the concurrency
732    /// runtime is imported from the shared `bock_runtime` module rather than
733    /// inlined. When clear, the module is emitted as a single self-contained file
734    /// with its runtime preludes inlined — the [`RustGenerator::generate_module`]
735    /// path used by unit tests.
736    per_module: bool,
737    /// Implicit cross-module imports for the per-module path, as
738    /// `(module_path, symbol_name)` pairs — public names this module references
739    /// but neither declares locally nor imports via an explicit `use` (e.g. a
740    /// §18.2-prelude trait used as an `impl` base). The `Module` arm emits a
741    /// `use crate::<module_path>::<symbol_name>;` for each. Computed in
742    /// `generate_project`.
743    implicit_imports: Vec<(String, String)>,
744    /// Armed while emitting the body of a function whose declared return type is
745    /// a `Fn(..) -> ..` (lowered to `impl Fn`). Promoted to
746    /// [`Self::returning_fn_closure`] only for the body's *tail* expression (the
747    /// returned value) by `emit_block_body`, so an intermediate `.map`/`.filter`
748    /// closure earlier in the body is unaffected.
749    return_closure_tail: bool,
750    /// Set only while emitting the *tail* expression of a closure-returning
751    /// function (see [`Self::return_closure_tail`]). A closure
752    /// (`Lambda`/`Compose`) produced here must `move`-capture its environment:
753    /// the returned `impl Fn` outlives the function frame, so borrowing a
754    /// captured param/local would be a dangling reference (E0373/E0507). The
755    /// function's `impl Fn` params additionally gain a `+ 'static` bound so the
756    /// moved captures satisfy the `'static` default of the returned `impl Fn`
757    /// (E0310).
758    returning_fn_closure: bool,
759    /// Names (original, not snake-cased) of top-level functions whose declared
760    /// return type is a `Fn(..) -> ..` (lowered to `impl Fn`). A `let` binding
761    /// whose RHS calls such a function holds a closure value, so it must not be
762    /// `.clone()`d on reuse (an `impl Fn` opaque type is not `Clone` — E0599);
763    /// it is borrowed instead. Populated once per program by
764    /// [`Self::collect_fn_returning_fns`].
765    fn_returning_fns: std::collections::HashSet<String>,
766    /// Snake-cased names of in-scope `let` bindings whose value is a function /
767    /// closure (`impl Fn`) — a `Lambda`/`Compose` RHS, an explicit `Fn(..)`
768    /// annotation, or a call to a [`Self::fn_returning_fns`] helper. A move-reuse
769    /// pass of such a binding is **borrowed** (`&f`) rather than cloned: `impl
770    /// Fn` is not `Clone` (E0599), but `&F: Fn` when `F: Fn`, so a borrow
771    /// satisfies an `impl Fn` parameter and leaves the binding live for the next
772    /// pass. Seeded per-block (saved/restored) so it never leaks.
773    fn_typed_bindings: std::collections::HashSet<String>,
774    /// Snake-cased names of in-scope `let` bindings whose value is a Rust
775    /// collection (`Vec`/`HashMap`/`HashSet`) — recognised from the binding's
776    /// RHS (`map.keys()`, a list literal, …) or a `List`/`Map`/`Set` type
777    /// annotation. A `Vec`/`HashMap`/`HashSet` does not implement
778    /// `std::fmt::Display`, so an interpolation of such a binding (`"keys=${keys}"`)
779    /// must use the `Debug` formatter (`{:?}`) instead of `{}` (E0277). Seeded
780    /// per-block in [`Self::emit_block_body`]; saved/restored so it never leaks.
781    collection_bindings: std::collections::HashSet<String>,
782    /// Maps a user `type` alias name (original, not snake-cased) whose RHS is a
783    /// `Fn(..) -> ..` to its underlying `TypeFunction` AIR node — e.g.
784    /// `type EventHandler = Fn() -> Void`. A signature position naming such an
785    /// alias is lowered to `impl Fn(..)` (param/return) exactly as a literal
786    /// `Fn(..)` would be: the alias resolves to a `fn` pointer, but the value
787    /// flowing through it is frequently a *capturing* closure (a closure that
788    /// captures does not coerce to `fn` — E0308). The `type` declaration itself
789    /// keeps the `fn`-pointer form (`impl Trait` is not nameable in a `type`
790    /// alias); only its *uses* in fn signatures widen. Populated once per program
791    /// by [`Self::collect_fn_type_aliases`].
792    fn_type_aliases: std::collections::HashMap<String, AIRNode>,
793    /// Snake-cased names of move-reused, non-`Copy` parameters whose declared
794    /// type is **concrete** (not a function's generic type parameter) — the
795    /// subset of [`Self::reused_let_bindings`] that is safe to `.clone()` when it
796    /// appears as a bare **value/block-tail** (`{ value }` arm) to avoid a
797    /// use-after-move (`E0382`). A generic `T` (e.g. `max_of<T: Ord>(a, b)`'s
798    /// `a`/`b`) is excluded: `T` has no `Clone` bound, so cloning it is `E0599`;
799    /// in a tail position the move is the value's *last* use anyway, so no clone
800    /// is needed there. Seeded alongside `reused_let_bindings` per function (see
801    /// [`Self::seed_reused_params`]) and restored together.
802    reused_value_tail_bindings: std::collections::HashSet<String>,
803    /// Snake-cased names of whole-scrutinee binding-arm patterns (`other => …`)
804    /// in the current `match` that must be re-bound from `&str` to an owned
805    /// `String` at the top of their arm body. Set only when [`Self::emit_match`]
806    /// matched a `String` scrutinee on `.as_str()` *because* the arms mix a
807    /// string-literal pattern with a whole-scrutinee bind (Q-rust-str-mixed-binding):
808    /// the `.as_str()` wrap retypes the bind to `&str`, so the arm body opens
809    /// with `let other = other.to_string();` to restore the `String` the Bock
810    /// binding has. Seeded per-`match` (saved/restored) so it never leaks to a
811    /// sibling/outer match.
812    str_rebind_match_binds: std::collections::HashSet<String>,
813}
814
815impl RsEmitCtx {
816    fn new() -> Self {
817        Self {
818            buf: String::with_capacity(4096),
819            indent: 0,
820            needs_rc_import: false,
821            needs_arc_import: false,
822            task_bound_names: std::collections::HashSet::new(),
823            effect_ops: HashMap::new(),
824            current_handler_vars: HashMap::new(),
825            borrowed_handler_effects: std::collections::HashSet::new(),
826            fn_effects: HashMap::new(),
827            composite_effects: HashMap::new(),
828            concurrency_runtime_emitted: false,
829            enum_variants: crate::generator::EnumVariantRegistry::new(),
830            generic_decls: crate::generator::GenericDeclRegistry::new(),
831            clone_target_records: std::collections::HashSet::new(),
832            clone_bound_records: std::collections::HashSet::new(),
833            user_equatable_types: std::collections::HashSet::new(),
834            in_clone_self_method: false,
835            in_assign_target: false,
836            self_operand_methods: std::collections::HashSet::new(),
837            reused_match_bindings: std::collections::HashSet::new(),
838            reused_let_bindings: std::collections::HashSet::new(),
839            trait_decls: crate::generator::TraitDeclRegistry::new(),
840            per_module: false,
841            implicit_imports: Vec::new(),
842            return_closure_tail: false,
843            returning_fn_closure: false,
844            fn_returning_fns: std::collections::HashSet::new(),
845            fn_typed_bindings: std::collections::HashSet::new(),
846            collection_bindings: std::collections::HashSet::new(),
847            fn_type_aliases: std::collections::HashMap::new(),
848            reused_value_tail_bindings: std::collections::HashSet::new(),
849            str_rebind_match_binds: std::collections::HashSet::new(),
850        }
851    }
852
853    /// Clone the cross-module *analysis* state (registries + the global
854    /// clone/self-operand sets) into a fresh emission context with an empty
855    /// buffer. Used by the per-module path to emit each module file from the
856    /// same pre-scanned program-wide context the bundling path built once, so a
857    /// reference in one file to a type/trait declared in another lowers
858    /// identically. The per-file state (`implicit_imports`, the runtime flag,
859    /// the buffer) starts fresh.
860    fn fork(&self) -> Self {
861        Self {
862            buf: String::with_capacity(4096),
863            indent: 0,
864            needs_rc_import: false,
865            needs_arc_import: false,
866            task_bound_names: std::collections::HashSet::new(),
867            effect_ops: self.effect_ops.clone(),
868            current_handler_vars: HashMap::new(),
869            borrowed_handler_effects: std::collections::HashSet::new(),
870            fn_effects: self.fn_effects.clone(),
871            composite_effects: self.composite_effects.clone(),
872            concurrency_runtime_emitted: false,
873            enum_variants: self.enum_variants.clone(),
874            generic_decls: self.generic_decls.clone(),
875            clone_target_records: self.clone_target_records.clone(),
876            clone_bound_records: self.clone_bound_records.clone(),
877            user_equatable_types: self.user_equatable_types.clone(),
878            in_clone_self_method: false,
879            in_assign_target: false,
880            self_operand_methods: self.self_operand_methods.clone(),
881            reused_match_bindings: std::collections::HashSet::new(),
882            reused_let_bindings: std::collections::HashSet::new(),
883            trait_decls: self.trait_decls.clone(),
884            per_module: false,
885            implicit_imports: Vec::new(),
886            return_closure_tail: false,
887            returning_fn_closure: false,
888            fn_returning_fns: self.fn_returning_fns.clone(),
889            fn_typed_bindings: std::collections::HashSet::new(),
890            collection_bindings: std::collections::HashSet::new(),
891            fn_type_aliases: self.fn_type_aliases.clone(),
892            reused_value_tail_bindings: std::collections::HashSet::new(),
893            str_rebind_match_binds: std::collections::HashSet::new(),
894        }
895    }
896
897    /// Populate [`Self::self_operand_methods`] from a trait registry: every
898    /// method (in any trait) whose own non-receiver params include a
899    /// `Self`-typed operand. These methods take that operand by shared
900    /// reference in Rust (see the field doc).
901    fn collect_self_operand_methods(&mut self, registry: &crate::generator::TraitDeclRegistry) {
902        for info in registry.values() {
903            for m in &info.methods {
904                let NodeKind::FnDecl { params, name, .. } = &m.kind else {
905                    continue;
906                };
907                let has_self_operand = params.iter().skip(1).any(|p| {
908                    matches!(
909                        &p.kind,
910                        NodeKind::Param { ty: Some(t), .. } if matches!(t.kind, NodeKind::TypeSelf)
911                    )
912                });
913                if has_self_operand {
914                    self.self_operand_methods.insert(name.name.clone());
915                }
916            }
917        }
918    }
919
920    /// Pre-scan a module's `impl` blocks and mark each *generic* record whose
921    /// impl returns a `self` field by value — those need `#[derive(Clone)]` and
922    /// a `T: Clone` impl bound because a `&self` method cannot move a non-`Copy`
923    /// field out. Returning `self.field` (Bock's by-value receiver consuming a
924    /// field) is lowered to `self.field.clone()`. Only generic targets are
925    /// considered: a concrete record returning a non-`Copy` field is the
926    /// pre-existing, orthogonal `&self` move-out defect, left untouched here.
927    fn collect_clone_targets(&mut self, module: &AIRModule) {
928        let NodeKind::Module { items, .. } = &module.kind else {
929            return;
930        };
931        for item in items {
932            let NodeKind::ImplBlock {
933                target, methods, ..
934            } = &item.kind
935            else {
936                continue;
937            };
938            let target_name = self.type_expr_to_string(target);
939            // Only generic targets (the `impl<T> Box<T>` synthesis case).
940            let is_generic = self
941                .generic_decls
942                .get(&target_name)
943                .is_some_and(|p| !p.is_empty());
944            if !is_generic {
945                continue;
946            }
947            let returns_self_field = methods.iter().any(Self::method_returns_self_field);
948            if returns_self_field {
949                self.clone_target_records.insert(target_name.clone());
950            }
951            // Record every generic record whose impl will carry a `T: Clone`
952            // bound, so a free generic fn taking it by value and driving its
953            // methods can propagate the bound (see `clone_bound_records`). This
954            // mirrors the impl-site `add_clone_bound` predicate: a field-return
955            // getter, a `self.field` move-out, or a generic-collection-element
956            // clone (`ListIterator.next` doing `self.xs.get(...)`).
957            let needs_clone_bound = returns_self_field
958                || methods.iter().any(|m| {
959                    matches!(&m.kind, NodeKind::FnDecl { body, .. }
960                        if Self::body_moves_self_field(body)
961                            || Self::body_clones_collection_element(body))
962                });
963            if needs_clone_bound {
964                self.clone_bound_records.insert(target_name);
965            }
966        }
967    }
968
969    /// Populate [`Self::user_equatable_types`] with every record/enum/class type
970    /// that carries the checker's [`bock_types::checker::CUSTOM_EQ_META_KEY`]
971    /// stamp (an explicit `impl Equatable`). These types get both an
972    /// `impl Equatable` and a delegating `impl PartialEq` (DQ31), so a desugared
973    /// `a.eq(&b)` on them must lower to the fully-qualified `Equatable::eq(&a,
974    /// &b)` to avoid the `PartialEq::eq`/`Equatable::eq` ambiguity (E0034).
975    /// (Q-rust-equatable-eq-collision.)
976    fn collect_user_equatable_types(&mut self, module: &AIRModule) {
977        let NodeKind::Module { items, .. } = &module.kind else {
978            return;
979        };
980        for item in items {
981            let name = match &item.kind {
982                NodeKind::RecordDecl { name, .. }
983                | NodeKind::EnumDecl { name, .. }
984                | NodeKind::ClassDecl { name, .. } => name,
985                _ => continue,
986            };
987            if matches!(
988                item.metadata.get(bock_types::checker::CUSTOM_EQ_META_KEY),
989                Some(bock_air::Value::Bool(true))
990            ) {
991                self.user_equatable_types.insert(name.name.clone());
992            }
993        }
994    }
995
996    /// Populate [`Self::fn_returning_fns`] with the names of top-level functions
997    /// whose declared return type is a `Fn(..) -> ..` (lowered to a non-`Clone`
998    /// `impl Fn`). A `let` binding whose RHS calls such a function (e.g.
999    /// `let pipeline = build_report_pipeline()`) then holds a closure value and
1000    /// must be borrowed, not cloned, on a move-reuse (E0599 — `impl Fn` is not
1001    /// `Clone`). See [`Self::fn_typed_bindings`].
1002    fn collect_fn_returning_fns(&mut self, module: &AIRModule) {
1003        let NodeKind::Module { items, .. } = &module.kind else {
1004            return;
1005        };
1006        for item in items {
1007            if let NodeKind::FnDecl {
1008                name, return_type, ..
1009            } = &item.kind
1010            {
1011                if return_type
1012                    .as_deref()
1013                    .is_some_and(|t| self.type_is_fn_closure(t))
1014                {
1015                    self.fn_returning_fns.insert(name.name.clone());
1016                }
1017            }
1018        }
1019    }
1020
1021    /// Populate [`Self::fn_type_aliases`] from a module's `type` declarations:
1022    /// every `type Name = Fn(..) -> ..` records `Name → <the TypeFunction node>`.
1023    /// A fn-signature position naming such an alias then lowers to `impl Fn(..)`
1024    /// (see [`Self::type_to_rs_fn_pos_bounded`] / [`Self::type_is_fn_closure`]),
1025    /// so a *capturing* closure value flowing through the alias (the common case
1026    /// — `with_logging` returning a closure that captures `name`/`handler`)
1027    /// type-checks; a bare `fn` pointer would reject it (E0308). The `type`
1028    /// declaration itself is unchanged (it keeps the `fn`-pointer form, which is
1029    /// the only `Fn`-shaped type nameable in a Rust `type` alias).
1030    fn collect_fn_type_aliases(&mut self, module: &AIRModule) {
1031        let NodeKind::Module { items, .. } = &module.kind else {
1032            return;
1033        };
1034        for item in items {
1035            if let NodeKind::TypeAlias { name, ty, .. } = &item.kind {
1036                if matches!(&ty.kind, NodeKind::TypeFunction { .. }) {
1037                    self.fn_type_aliases
1038                        .insert(name.name.clone(), (**ty).clone());
1039                }
1040            }
1041        }
1042    }
1043
1044    /// True when a type, resolving through any `Fn`-typed `type` alias
1045    /// ([`Self::fn_type_aliases`]), is a function type `Fn(..) -> ..` — the
1046    /// signal to lower it to `impl Fn(..)` in a param/return position and to
1047    /// treat a function returning it as a closure-returning function. A literal
1048    /// `TypeFunction` qualifies directly; a `TypeNamed` qualifies when it names a
1049    /// registered `Fn`-typed alias.
1050    fn type_is_fn_closure(&self, ty: &AIRNode) -> bool {
1051        self.resolve_fn_closure_type(ty).is_some()
1052    }
1053
1054    /// If `ty` is (or, through a `Fn`-typed alias, resolves to) a function type,
1055    /// return the underlying `TypeFunction` node; otherwise `None`. Resolution
1056    /// follows one alias hop, which is all v1 produces (an alias whose RHS is a
1057    /// bare `Fn(..)`); a chain of aliases is not expected and falls through.
1058    fn resolve_fn_closure_type<'a>(&'a self, ty: &'a AIRNode) -> Option<&'a AIRNode> {
1059        match &ty.kind {
1060            NodeKind::TypeFunction { .. } => Some(ty),
1061            NodeKind::TypeNamed { path, args } if args.is_empty() => {
1062                let name = path.segments.last()?;
1063                self.fn_type_aliases.get(&name.name)
1064            }
1065            _ => None,
1066        }
1067    }
1068
1069    /// True when a `let`-binding RHS produces a function / closure value (`impl
1070    /// Fn`) — a `Lambda`, a `Compose` (`f >> g`), or a call to a
1071    /// [`Self::fn_returning_fns`] helper. Such a binding is borrowed (not cloned)
1072    /// on a move-reuse. A conservative syntactic probe; when unsure it returns
1073    /// `false` (the binding keeps the default clone-or-move path).
1074    fn rhs_is_fn_valued(&self, value: &AIRNode) -> bool {
1075        match &value.kind {
1076            NodeKind::Lambda { .. } | NodeKind::Compose { .. } => true,
1077            NodeKind::Call { callee, .. } => {
1078                matches!(&callee.kind, NodeKind::Identifier { name }
1079                    if self.fn_returning_fns.contains(&name.name))
1080            }
1081            _ => false,
1082        }
1083    }
1084
1085    /// True when a method's body returns a bare `self.field` by value — either an
1086    /// explicit `return self.field` or a `self.field` block-tail. Such a return
1087    /// moves the field out of the `&self` receiver and so requires a clone (and a
1088    /// `Clone` bound) under Rust's borrow rules.
1089    fn method_returns_self_field(method: &AIRNode) -> bool {
1090        let NodeKind::FnDecl { body, .. } = &method.kind else {
1091            return false;
1092        };
1093        Self::block_returns_self_field(body)
1094    }
1095
1096    /// Does this node, in value/return position, evaluate to a `self.field`?
1097    fn block_returns_self_field(node: &AIRNode) -> bool {
1098        match &node.kind {
1099            NodeKind::Block { stmts, tail } => {
1100                if let Some(t) = tail {
1101                    // The tail may be a bare `self.field` (implicit return) or a
1102                    // `return self.field;` statement (Bock allows an explicit
1103                    // `return` in tail position).
1104                    if Self::is_self_field(t) || Self::stmt_returns_self_field(t) {
1105                        return true;
1106                    }
1107                }
1108                stmts.iter().any(Self::stmt_returns_self_field)
1109            }
1110            _ => Self::is_self_field(node),
1111        }
1112    }
1113
1114    /// A `return self.field;` statement (or a nested block/return that does).
1115    fn stmt_returns_self_field(node: &AIRNode) -> bool {
1116        match &node.kind {
1117            NodeKind::Return { value: Some(v) } => Self::is_self_field(v),
1118            NodeKind::Block { .. } => Self::block_returns_self_field(node),
1119            _ => false,
1120        }
1121    }
1122
1123    /// True when `node` is exactly `self.<field>`.
1124    fn is_self_field(node: &AIRNode) -> bool {
1125        matches!(
1126            &node.kind,
1127            NodeKind::FieldAccess { object, .. }
1128                if matches!(&object.kind, NodeKind::Identifier { name } if name.name == "self")
1129        )
1130    }
1131
1132    /// True when this fn/method body, in value/return position, evaluates to an
1133    /// expression that *contains* a `self.field` read — either a bare
1134    /// `self.field` or a `self.field` wrapped in a constructor such as
1135    /// `Some(self.field)` / `Ok(self.field)` / a record or enum-variant build.
1136    ///
1137    /// Such a return moves the field out of the `&self` receiver, which Rust's
1138    /// borrow checker forbids for a non-`Copy` field; the codegen lowers the
1139    /// `self.field` read to `self.field.clone()` (gated on
1140    /// [`Self::in_clone_self_method`]) and the impl/fn carries a `T: Clone`
1141    /// bound. This generalises [`Self::block_returns_self_field`] (a *bare*
1142    /// `return self.field`) to the wrapped case `return Some(self.v)`, the shape
1143    /// a generic `fn f(self) -> Optional[T]` produces.
1144    ///
1145    /// Crucially it inspects only return/tail *value* positions, never a
1146    /// statement such as `self.cursor = self.cursor + 1` (whose `self.cursor`
1147    /// reads must NOT be cloned — the assignment LHS would become an invalid
1148    /// `self.cursor.clone() = ...`).
1149    fn body_moves_self_field(node: &AIRNode) -> bool {
1150        match &node.kind {
1151            NodeKind::Block { stmts, tail } => {
1152                if let Some(t) = tail {
1153                    if Self::expr_contains_self_field(t) || Self::body_moves_self_field(t) {
1154                        return true;
1155                    }
1156                }
1157                stmts.iter().any(Self::body_moves_self_field)
1158            }
1159            NodeKind::Return { value: Some(v) } => Self::expr_contains_self_field(v),
1160            // A `let x = … self.field …` RHS moves the field by value out of the
1161            // `&self` receiver just as a return does (`E0507`) — e.g.
1162            // `let tag = type_tag(self.msg_type)` in a trait `serialize(self)`.
1163            // The RHS is a value position (never an assignment LHS), so cloning
1164            // the `self.field` read there is sound.
1165            NodeKind::LetBinding { value, .. } => Self::expr_contains_self_field(value),
1166            // A bare free-function call statement that passes `self.field` by
1167            // value (`emit(self.payload)`) moves it out too. A `MethodCall` is
1168            // excluded: its `self.field.method()` receiver *borrows* (methods
1169            // lower to `&self`), so it is not a move. `Assign` is excluded as
1170            // well — its target is a place expression whose `self.field` must NOT
1171            // clone (the `in_assign_target` guard also defends the emit site).
1172            NodeKind::Call { .. } => Self::expr_contains_self_field(node),
1173            // Control-flow whose arms carry value/return positions worth
1174            // descending into (e.g. a `match` whose arms `return Some(self.v)`).
1175            NodeKind::If {
1176                then_block,
1177                else_block,
1178                ..
1179            } => {
1180                Self::body_moves_self_field(then_block)
1181                    || else_block
1182                        .as_ref()
1183                        .is_some_and(|e| Self::body_moves_self_field(e))
1184            }
1185            NodeKind::Match { arms, .. } => arms.iter().any(|arm| {
1186                if let NodeKind::MatchArm { body, .. } = &arm.kind {
1187                    Self::expr_contains_self_field(body) || Self::body_moves_self_field(body)
1188                } else {
1189                    false
1190                }
1191            }),
1192            _ => false,
1193        }
1194    }
1195
1196    /// True when `node` (an expression in value position) reads a `self.field`
1197    /// directly or via a wrapping constructor call / aggregate. Deliberately
1198    /// conservative: it descends through `Call` arguments (the `Some(self.v)`
1199    /// case) and record/aggregate fields, but treats the read as a move only
1200    /// when it is genuinely a `self.field` access, not e.g. `self.field.method()`
1201    /// (a method call borrows rather than moves) or a comparison.
1202    fn expr_contains_self_field(node: &AIRNode) -> bool {
1203        if Self::is_self_field(node) {
1204            return true;
1205        }
1206        match &node.kind {
1207            // `Some(self.v)`, `Ok(self.v)`, `Variant(self.v)`, `f(self.v)` — the
1208            // field flows by value into the constructed/returned value.
1209            NodeKind::Call { args, .. } => args
1210                .iter()
1211                .any(|a| Self::expr_contains_self_field(&a.value)),
1212            NodeKind::RecordConstruct { fields, .. } => fields.iter().any(|f| {
1213                f.value
1214                    .as_deref()
1215                    .is_some_and(Self::expr_contains_self_field)
1216            }),
1217            NodeKind::TupleLiteral { elems } | NodeKind::ListLiteral { elems } => {
1218                elems.iter().any(Self::expr_contains_self_field)
1219            }
1220            _ => false,
1221        }
1222    }
1223
1224    /// True when this fn/method body will emit a `.clone()` / `.cloned()` on a
1225    /// *generic* element value via a built-in collection method — `List.get` /
1226    /// `first` / `last` / `concat`, `Map.get` / `keys` / `values`, or a `Set`
1227    /// algebraic op. Each lowers to a `.cloned()` (or `.clone()` for `concat`)
1228    /// over the receiver's element type; when that element type is a generic
1229    /// param the impl/fn needs a `T: Clone` bound (the v1 concrete element types
1230    /// Int/Float/String/Bool all satisfy it).
1231    ///
1232    /// Detection is conservative on the *operation* (does the body call a
1233    /// clone-inducing built-in at all) rather than precisely typing each
1234    /// receiver's element — for a generic fn/impl over `List[T]`, the element
1235    /// flowing through these calls is always the generic param. A clone bound on
1236    /// a generic param that happens not to need it is harmless (every concrete
1237    /// instantiation in v1 is `Clone`); the gate is correctness, and the
1238    /// detection never fires for a body that emits no such call.
1239    fn body_clones_collection_element(body: &AIRNode) -> bool {
1240        struct CloneScan {
1241            found: bool,
1242        }
1243        impl bock_air::visitor::Visitor for CloneScan {
1244            fn visit_node(&mut self, node: &AIRNode) {
1245                if self.found {
1246                    return;
1247                }
1248                if let NodeKind::Call { callee, args, .. } = &node.kind {
1249                    if let Some((_, method, _)) =
1250                        crate::generator::desugared_list_method(node, callee, args)
1251                    {
1252                        if matches!(method, "get" | "first" | "last" | "concat") {
1253                            self.found = true;
1254                            return;
1255                        }
1256                    }
1257                    // The functional combinators all lower through `.iter()
1258                    // .cloned()` / `.clone().into_iter()`, so they clone the
1259                    // element type just like `get`/`concat` above.
1260                    if crate::generator::desugared_list_functional_method(node, callee, args)
1261                        .is_some()
1262                    {
1263                        self.found = true;
1264                        return;
1265                    }
1266                    if let Some((_, method, _)) =
1267                        crate::generator::desugared_map_method(node, callee, args)
1268                    {
1269                        if matches!(method, "get" | "keys" | "values") {
1270                            self.found = true;
1271                            return;
1272                        }
1273                    }
1274                    if let Some((_, method, _)) =
1275                        crate::generator::desugared_set_method(node, callee, args)
1276                    {
1277                        if matches!(method, "union" | "intersection" | "difference" | "to_list") {
1278                            self.found = true;
1279                            return;
1280                        }
1281                    }
1282                }
1283                bock_air::visitor::walk_node(self, node);
1284            }
1285        }
1286        let mut scan = CloneScan { found: false };
1287        bock_air::visitor::Visitor::visit_node(&mut scan, body);
1288        scan.found
1289    }
1290
1291    /// True when some `match` arm in `body` binds a pattern variable the arm
1292    /// reads **more than once** — the case the runtime move-reuse analysis
1293    /// ([`Self::reused_match_bindings`]) lowers by emitting `<x>.clone()` on each
1294    /// by-value use after the first (the Rust pattern binds by value, so the
1295    /// first by-value consumer moves it; later uses would be `E0382`). When the
1296    /// reused binding is a *generic* element (`filter[T](o: Optional[T], pred:
1297    /// Fn(T) -> Bool)` doing `match o { Some(x) => if pred(x) { Some(x) } … }`),
1298    /// the emitted `x.clone()` needs `T: Clone` in scope, so the enclosing
1299    /// generic fn must carry the bound — otherwise `E0599`/`E0277`.
1300    ///
1301    /// Conservative: it fires on the *shape* (a match arm with a reused binding)
1302    /// rather than typing each binding, mirroring
1303    /// [`Self::body_clones_collection_element`]. A `T: Clone` bound on a generic
1304    /// param that turns out not to need it is harmless (every concrete v1
1305    /// element type — Int/Float/String/Bool/nested — is `Clone`), and the scan
1306    /// never fires for a body whose match arms each use their bindings at most
1307    /// once (`or_else`/`to_list`/`count`/`get_or` over `Optional` all stay
1308    /// unconstrained, matching the pre-existing behaviour). The caller gates this
1309    /// on `!generic_params.is_empty()` so a non-generic fn is never touched.
1310    fn body_reuses_match_binding(body: &AIRNode) -> bool {
1311        struct ReuseScan {
1312            found: bool,
1313        }
1314        impl bock_air::visitor::Visitor for ReuseScan {
1315            fn visit_node(&mut self, node: &AIRNode) {
1316                if self.found {
1317                    return;
1318                }
1319                if let NodeKind::MatchArm { pattern, body, .. } = &node.kind {
1320                    let mut bound = Vec::new();
1321                    RsEmitCtx::collect_pattern_binding_names(pattern, &mut bound);
1322                    for name in &bound {
1323                        if RsEmitCtx::count_identifier_uses(body, name) > 1 {
1324                            self.found = true;
1325                            return;
1326                        }
1327                    }
1328                }
1329                bock_air::visitor::walk_node(self, node);
1330            }
1331        }
1332        let mut scan = ReuseScan { found: false };
1333        bock_air::visitor::Visitor::visit_node(&mut scan, body);
1334        scan.found
1335    }
1336
1337    /// True when a *generic* free function takes a parameter whose base type is
1338    /// a clone-bound record ([`Self::clone_bound_records`] — a record whose
1339    /// `impl` carries a `T: Clone` bound, e.g. `ListIterator[T]`) and drives it
1340    /// with at least one method call. Such a function must propagate the
1341    /// record's `T: Clone` bound to its own signature, or method resolution
1342    /// fails (`count[T](it: ListIterator[T])` calling `it.next()` →
1343    /// `E0599`: the method exists but its trait bounds are not satisfied).
1344    ///
1345    /// Conservative on both halves: the param must base-resolve to a recorded
1346    /// clone-bound record (never a built-in collection or a non-generic record),
1347    /// AND the body must contain a `MethodCall` (driving the record) — a fn that
1348    /// merely receives such a record but never calls a method on it emits no
1349    /// bound-requiring code and is left un-constrained.
1350    fn params_drive_clone_bound_record(&self, params: &[AIRNode], body: &AIRNode) -> bool {
1351        let takes_clone_bound_record = params.iter().any(|p| {
1352            let NodeKind::Param { ty: Some(t), .. } = &p.kind else {
1353                return false;
1354            };
1355            self.clone_bound_records
1356                .contains(&self.type_expr_base_name(t))
1357        });
1358        if !takes_clone_bound_record {
1359            return false;
1360        }
1361        struct MethodCallScan {
1362            found: bool,
1363        }
1364        impl bock_air::visitor::Visitor for MethodCallScan {
1365            fn visit_node(&mut self, node: &AIRNode) {
1366                if self.found {
1367                    return;
1368                }
1369                // A user method call (`cur.next()`) lowers to a `Call` whose
1370                // callee is a `FieldAccess` (the lowerer's desugared-self-call
1371                // shape — see `generator::desugared_self_call`), not a
1372                // `MethodCall` node; the bare `MethodCall` variant never reaches
1373                // codegen for these. Treat either form as "drives a method".
1374                let is_call_on_member = matches!(&node.kind,
1375                    NodeKind::Call { callee, .. }
1376                        if matches!(callee.kind, NodeKind::FieldAccess { .. }));
1377                if is_call_on_member || matches!(node.kind, NodeKind::MethodCall { .. }) {
1378                    self.found = true;
1379                    return;
1380                }
1381                bock_air::visitor::walk_node(self, node);
1382            }
1383        }
1384        let mut scan = MethodCallScan { found: false };
1385        bock_air::visitor::Visitor::visit_node(&mut scan, body);
1386        scan.found
1387    }
1388
1389    /// The `Enum::` qualifier for a variant *path* if its last segment is a
1390    /// registered user enum variant, else `None`. The built-in
1391    /// `Optional`/`Result` pre-seeds are intentionally excluded here: their
1392    /// constructions and patterns are handled by the bespoke Rust lowering
1393    /// (`Some(x)`/`None`/`Ok`/`Err` map to `std::option`/`std::result`), which
1394    /// must not be rewritten to `Optional::Some`.
1395    fn variant_enum_qualifier(&self, path: &bock_ast::TypePath) -> Option<String> {
1396        let info = crate::generator::registered_variant(&self.enum_variants, path)?;
1397        if matches!(info.enum_name.as_str(), "Optional" | "Result") {
1398            return None;
1399        }
1400        Some(info.enum_name.clone())
1401    }
1402
1403    /// As [`Self::variant_enum_qualifier`] but for a bare identifier name (a
1404    /// unit-variant construction lowers to `Identifier`, or a tuple-variant
1405    /// construction's callee is an `Identifier`).
1406    fn variant_enum_qualifier_for_name(&self, name: &str) -> Option<String> {
1407        let info = self.enum_variants.get(name)?;
1408        if matches!(info.enum_name.as_str(), "Optional" | "Result") {
1409            return None;
1410        }
1411        Some(info.enum_name.clone())
1412    }
1413
1414    /// True when the real `core.compare.Ordering` enum is reachable in this
1415    /// program (its `Less` variant is a registered user enum variant). When
1416    /// `core.compare` is `use`d, the actual `enum Ordering` decl is emitted; the
1417    /// `Less`/`Equal`/`Greater` references and match patterns must then use that
1418    /// user enum (`Ordering::Less`), not the `std::cmp::Ordering` bridge the
1419    /// prelude form uses when the enum is *not* reachable (e.g. a bare primitive
1420    /// `compare`).
1421    fn ordering_enum_reachable(&self) -> bool {
1422        self.enum_variants
1423            .get("Less")
1424            .is_some_and(|info| info.enum_name == "Ordering")
1425    }
1426
1427    fn finish(mut self) -> String {
1428        if self.buf.is_empty() {
1429            return self.buf;
1430        }
1431        // rustfmt wraps an inner-attribute list of this many items across lines
1432        // (regardless of the line fitting in `max_width`), so emit the wrapped
1433        // form directly — the §20.6.2 codegen-formatter agreement requires the
1434        // output to pass `rustfmt --check` cleanly on first generation (S7).
1435        let mut prefix = String::from(
1436            "#![allow(\n    unused_variables,\n    unused_imports,\n    unused_parens,\n    dead_code,\n    non_upper_case_globals\n)]\n\n",
1437        );
1438        if self.needs_rc_import {
1439            prefix.push_str("use std::rc::Rc;\n");
1440        }
1441        if self.needs_arc_import {
1442            prefix.push_str("use std::sync::Arc;\n");
1443        }
1444        if !prefix.ends_with("\n\n") {
1445            prefix.push('\n');
1446        }
1447        self.buf.insert_str(0, &prefix);
1448        self.buf
1449    }
1450
1451    /// Finish one file of the per-module native tree (S3): prepend the per-file
1452    /// `#![allow(...)]` inner attribute and any `use std::{rc,sync}` the body
1453    /// needs, then return the buffer. The cross-module `use crate::<m>::<x>;`
1454    /// statements (and the shared-runtime `use`) are emitted into the buffer by
1455    /// the `Module` arm, so they already sit at the top of the body — this only
1456    /// adds the crate/std-level preamble. `#![allow(...)]` is a module-level
1457    /// inner attribute valid at the head of any module file (the crate root
1458    /// `main.rs` *and* a submodule like `src/core/option.rs`).
1459    fn finish_per_module(mut self) -> String {
1460        if self.buf.is_empty() {
1461            return self.buf;
1462        }
1463        // rustfmt wraps an inner-attribute list of this many items across lines
1464        // (regardless of the line fitting in `max_width`), so emit the wrapped
1465        // form directly — the §20.6.2 codegen-formatter agreement requires the
1466        // output to pass `rustfmt --check` cleanly on first generation (S7).
1467        let mut prefix = String::from(
1468            "#![allow(\n    unused_variables,\n    unused_imports,\n    unused_parens,\n    dead_code,\n    non_upper_case_globals\n)]\n\n",
1469        );
1470        if self.needs_rc_import {
1471            prefix.push_str("use std::rc::Rc;\n");
1472        }
1473        if self.needs_arc_import {
1474            prefix.push_str("use std::sync::Arc;\n");
1475        }
1476        if !prefix.ends_with("\n\n") {
1477            prefix.push('\n');
1478        }
1479        self.buf.insert_str(0, &prefix);
1480        self.buf
1481    }
1482
1483    /// Pre-seed the effect registries (`effect_ops`, `composite_effects`) from
1484    /// every module's top-level `EffectDecl`s. In the per-module path each
1485    /// module is emitted by its own forked context, so a bare op `log(...)` used
1486    /// in `main` whose effect `Log` is declared in another module would not be
1487    /// recognised as an effect op (and not rewritten to `__handler.log(...)`)
1488    /// without pre-seeding from the whole reachable set. Mirrors how
1489    /// `enum_variants` / `trait_decls` are collected across the reached modules
1490    /// and the Python / JS / TS backends' equivalents.
1491    fn seed_effect_registries(&mut self, modules: &[(&AIRModule, &std::path::Path)]) {
1492        for (module, _) in modules {
1493            let NodeKind::Module { items, .. } = &module.kind else {
1494                continue;
1495            };
1496            for item in items {
1497                let NodeKind::EffectDecl {
1498                    name,
1499                    components,
1500                    operations,
1501                    ..
1502                } = &item.kind
1503                else {
1504                    continue;
1505                };
1506                if !components.is_empty() {
1507                    let comp_names: Vec<String> = components
1508                        .iter()
1509                        .map(|tp| {
1510                            tp.segments
1511                                .last()
1512                                .map_or("effect".to_string(), |s| s.name.clone())
1513                        })
1514                        .collect();
1515                    self.composite_effects.insert(name.name.clone(), comp_names);
1516                    continue;
1517                }
1518                for op in operations {
1519                    if let NodeKind::FnDecl { name: op_name, .. } = &op.kind {
1520                        self.effect_ops
1521                            .insert(op_name.name.clone(), name.name.clone());
1522                    }
1523                }
1524            }
1525        }
1526    }
1527
1528    /// Emit the per-module cross-module `use crate::<m>::<x>;` statements at the
1529    /// top of the file: the explicit `use`d symbols and the implicit
1530    /// §18.2-prelude names this module references but does not `use`. Grouped
1531    /// one `use crate::<path>::{a, b};` per source module, deterministically
1532    /// ordered. The dotted declared path `core.option` becomes the crate path
1533    /// `crate::core::option`.
1534    ///
1535    /// Built-in prelude *value/type* names that lower to native Rust
1536    /// (`Optional`/`Result` → `Option`/`Result`, `Some`/`None`/`Ok`/`Err`) are
1537    /// skipped — they are not real exports of the declaring stdlib module, so a
1538    /// `use crate::core::option::Some;` would not resolve. Cross-module
1539    /// references to those resolve through the native lowering instead.
1540    ///
1541    /// A braced enum **variant** (`use core.compare.{Ordering, Less, ...}`) is
1542    /// likewise not emitted as a free `use` item: Rust reaches a variant only as
1543    /// `Enum::Variant` (a bare `use crate::core::compare::Less;` is
1544    /// `E0432: unresolved import`). Such an item is replaced by its enum *type*
1545    /// under the same module path, so the type is in scope and the variant's use
1546    /// sites — which already emit `Ordering::Less` — resolve.
1547    fn emit_cross_module_uses(&mut self, imports: &[AIRNode]) {
1548        use std::collections::BTreeMap;
1549        // crate-path → set of leaf symbol names (sorted, deduped on render).
1550        let mut by_module: BTreeMap<String, std::collections::BTreeSet<String>> = BTreeMap::new();
1551
1552        // Explicit `use mod.{a, b}` imports.
1553        for import in imports {
1554            let NodeKind::ImportDecl { path, items } = &import.kind else {
1555                continue;
1556            };
1557            let dotted = path
1558                .segments
1559                .iter()
1560                .map(|s| s.name.as_str())
1561                .collect::<Vec<_>>()
1562                .join(".");
1563            if dotted.is_empty() {
1564                continue;
1565            }
1566            if let bock_ast::ImportItems::Named(named) = items {
1567                for n in named {
1568                    let item = n.name.name.as_str();
1569                    if RS_NATIVE_PRELUDE_NAMES.contains(&item) {
1570                        continue;
1571                    }
1572                    // An enum VARIANT is not a free-importable item in Rust
1573                    // (E0432): a variant is reached as `Enum::Variant`, never
1574                    // through `use crate::<m>::Variant`. Drop the braced variant
1575                    // and import its enum TYPE instead, under the same declaring
1576                    // module path the variant was `use`d from — the variant's
1577                    // use sites already emit `Enum::Variant`, so having the type
1578                    // in scope is all that's needed. (The built-in
1579                    // Optional/Result variants never reach here — they are in the
1580                    // native-prelude skip set above.)
1581                    if let Some(info) = self.enum_variants.get(item) {
1582                        by_module
1583                            .entry(dotted.clone())
1584                            .or_default()
1585                            .insert(info.enum_name.clone());
1586                        continue;
1587                    }
1588                    by_module
1589                        .entry(dotted.clone())
1590                        .or_default()
1591                        .insert(item.to_string());
1592                }
1593            }
1594            // `use Foo` / `use Foo.*`: the referenced names are resolved as
1595            // implicit imports below, so no statement is needed for the bare
1596            // module/glob form.
1597        }
1598
1599        // Implicit imports: prelude-visible names referenced but not `use`d.
1600        for (module_path, name) in &self.implicit_imports {
1601            if RS_NATIVE_PRELUDE_NAMES.contains(&name.as_str()) {
1602                continue;
1603            }
1604            by_module
1605                .entry(module_path.clone())
1606                .or_default()
1607                .insert(name.clone());
1608        }
1609
1610        for (dotted, names) in by_module {
1611            if names.is_empty() {
1612                continue;
1613            }
1614            let crate_path = format!("crate::{}", dotted.replace('.', "::"));
1615            let joined = names.into_iter().collect::<Vec<_>>().join(", ");
1616            self.writeln(&format!("use {crate_path}::{{{joined}}};"));
1617        }
1618    }
1619
1620    fn indent_str(&self) -> String {
1621        "    ".repeat(self.indent)
1622    }
1623
1624    fn write_indent(&mut self) {
1625        let indent = self.indent_str();
1626        self.buf.push_str(&indent);
1627    }
1628
1629    fn writeln(&mut self, s: &str) {
1630        self.write_indent();
1631        self.buf.push_str(s);
1632        self.buf.push('\n');
1633    }
1634
1635    // ── Prelude function mapping ──────────────────────────────────────────
1636
1637    /// Emit an expression into a temporary buffer and return the string.
1638    fn expr_to_string(&mut self, node: &AIRNode) -> Result<String, CodegenError> {
1639        let start = self.buf.len();
1640        self.emit_expr(node)?;
1641        let s = self.buf[start..].to_string();
1642        self.buf.truncate(start);
1643        Ok(s)
1644    }
1645
1646    /// Map Bock prelude functions to Rust equivalents.
1647    fn map_prelude_call(
1648        &mut self,
1649        callee: &AIRNode,
1650        args: &[bock_air::AirArg],
1651    ) -> Result<Option<String>, CodegenError> {
1652        let name = match &callee.kind {
1653            NodeKind::Identifier { name } => name.name.as_str(),
1654            _ => return Ok(None),
1655        };
1656        let arg_strs: Vec<String> = args
1657            .iter()
1658            .map(|a| self.expr_to_string(&a.value))
1659            .collect::<Result<_, _>>()?;
1660        let code = match name {
1661            "println" => {
1662                let a = arg_strs.first().map_or(String::new(), |s| s.clone());
1663                format!("println!(\"{{}}\", {a})")
1664            }
1665            "print" => {
1666                let a = arg_strs.first().map_or(String::new(), |s| s.clone());
1667                format!("print!(\"{{}}\", {a})")
1668            }
1669            "debug" => {
1670                let a = arg_strs.first().map_or(String::new(), |s| s.clone());
1671                format!("dbg!(&{a})")
1672            }
1673            "assert" => {
1674                let a = arg_strs.first().map_or(String::new(), |s| s.clone());
1675                format!("assert!({a})")
1676            }
1677            "todo" => "todo!()".to_string(),
1678            "unreachable" => "unreachable!()".to_string(),
1679            "sleep" => {
1680                let a = arg_strs.first().map_or(String::new(), |s| s.clone());
1681                // Route through an installed `Clock` handler if one is in scope;
1682                // otherwise fall through to the host primitive (default).
1683                if let Some(handler) = self.clock_handler_var() {
1684                    format!("{handler}.{}({a})", to_snake_case("sleep"))
1685                } else {
1686                    format!("tokio::time::sleep(std::time::Duration::from_nanos(({a}) as u64))")
1687                }
1688            }
1689            _ => return Ok(None),
1690        };
1691        Ok(Some(code))
1692    }
1693
1694    /// Emit a built-in `Optional`/`Result` method call to its Rust form.
1695    ///
1696    /// Bock `Optional[T]`/`Result[T, E]` lower to Rust's native `Option<T>` /
1697    /// `Result<T, E>`, and the built-in methods are (nearly) the native methods,
1698    /// so this is mostly a name passthrough — *except* it (a) clones the receiver
1699    /// for the by-value (consuming) methods (`unwrap`/`unwrap_or`/`map`/…) so a
1700    /// later `r.is_ok()` does not hit a borrow-of-moved-value error when the same
1701    /// value is read again, and (b) renames `flat_map` → the native `and_then`.
1702    /// `T: Clone` holds for the v1 payload types (Int/Float/String/Bool/nested
1703    /// Option/Result). Recognised via the checker's `recv_kind` annotation.
1704    /// Returns `true` if handled.
1705    fn try_emit_container_method(
1706        &mut self,
1707        node: &AIRNode,
1708        callee: &AIRNode,
1709        args: &[bock_air::AirArg],
1710    ) -> Result<bool, CodegenError> {
1711        let resolved = crate::generator::desugared_optional_method(node, callee, args)
1712            .or_else(|| crate::generator::desugared_result_method(node, callee, args));
1713        let Some((recv, method, rest)) = resolved else {
1714            return Ok(false);
1715        };
1716        // `is_*` take `&self` (no move); the rest consume `self`, so clone the
1717        // receiver to keep it usable afterwards.
1718        let consuming = !matches!(method, "is_some" | "is_none" | "is_ok" | "is_err");
1719        let native = match method {
1720            "flat_map" => "and_then",
1721            other => other,
1722        };
1723        self.buf.push('(');
1724        self.emit_expr(recv)?;
1725        self.buf.push(')');
1726        if consuming {
1727            self.buf.push_str(".clone()");
1728        }
1729        let _ = write!(self.buf, ".{native}(");
1730        for (i, arg) in rest.iter().enumerate() {
1731            if i > 0 {
1732                self.buf.push_str(", ");
1733            }
1734            self.emit_expr(&arg.value)?;
1735        }
1736        self.buf.push(')');
1737        Ok(true)
1738    }
1739
1740    /// Emit a read-only `List` built-in method call to its Rust form.
1741    ///
1742    /// Lists are `Vec<T>`. `len`/`length`/`count` coerce `usize` → `i64`
1743    /// (`(r.len() as i64)`) so the result composes with Bock's `Int`.
1744    /// `Optional`-returning methods use Rust's *native* `Option<T>` (the rep the
1745    /// Rust `match` lowering already expects): `get` is `r.get(i as
1746    /// usize).cloned()`, `first`/`last` are `r.first()/last().cloned()`, and
1747    /// `index_of` maps the found position to `i64`. `.cloned()` requires
1748    /// `T: Clone`, which the v1 element types (Int/Float/String/Bool) satisfy.
1749    /// `concat` is a functional clone-and-extend.
1750    fn try_emit_list_method(
1751        &mut self,
1752        node: &AIRNode,
1753        callee: &AIRNode,
1754        args: &[bock_air::AirArg],
1755    ) -> Result<bool, CodegenError> {
1756        let Some((recv, method, rest)) =
1757            crate::generator::desugared_list_method(node, callee, args)
1758        else {
1759            return Ok(false);
1760        };
1761        let recv_str = self.expr_to_string(recv)?;
1762        let code = match method {
1763            "len" | "length" | "count" => format!("(({recv_str}).len() as i64)"),
1764            "is_empty" => format!("({recv_str}).is_empty()"),
1765            "get" => {
1766                let Some(idx) = rest.first() else {
1767                    return Ok(false);
1768                };
1769                let i = self.expr_to_string(&idx.value)?;
1770                format!("({recv_str}).get(({i}) as usize).cloned()")
1771            }
1772            "first" => format!("({recv_str}).first().cloned()"),
1773            "last" => format!("({recv_str}).last().cloned()"),
1774            "contains" => {
1775                let Some(x) = rest.first() else {
1776                    return Ok(false);
1777                };
1778                let x = self.expr_to_string(&x.value)?;
1779                format!("({recv_str}).contains(&({x}))")
1780            }
1781            "index_of" => {
1782                let Some(x) = rest.first() else {
1783                    return Ok(false);
1784                };
1785                let x = self.expr_to_string(&x.value)?;
1786                format!("({recv_str}).iter().position(|__e| __e == &({x})).map(|__i| __i as i64)")
1787            }
1788            "concat" => {
1789                let Some(o) = rest.first() else {
1790                    return Ok(false);
1791                };
1792                let o = self.expr_to_string(&o.value)?;
1793                format!(
1794                    "{{ let mut __v = ({recv_str}).clone(); __v.extend(({o}).iter().cloned()); __v }}"
1795                )
1796            }
1797            "join" => {
1798                let Some(sep) = rest.first() else {
1799                    return Ok(false);
1800                };
1801                let sep = self.expr_to_string(&sep.value)?;
1802                format!("({recv_str}).join(({sep}).as_str())")
1803            }
1804            _ => return Ok(false),
1805        };
1806        self.buf.push_str(&code);
1807        Ok(true)
1808    }
1809
1810    /// Emit an in-place `List` mutator (`push`/`append`, DQ18) to its Rust form.
1811    ///
1812    /// Recognised via [`crate::generator::desugared_list_mutating_method`]. Bock
1813    /// `List[T]` lowers to `Vec<T>`, whose `push` is `&mut self`, so this emits
1814    /// `(recv).push(x)` against the receiver *place* (not a clone — these methods
1815    /// mutate in place). The checker types these as `Void`, so they appear in
1816    /// statement position; the ownership pass guarantees the receiver is a `mut`
1817    /// lvalue, and the `let mut` / `mut` parameter binding (whose `is_mut` flag
1818    /// this backend already honours when emitting the binding) makes the place
1819    /// `&mut`-able.
1820    fn try_emit_list_mutating_method(
1821        &mut self,
1822        node: &AIRNode,
1823        callee: &AIRNode,
1824        args: &[bock_air::AirArg],
1825    ) -> Result<bool, CodegenError> {
1826        let Some((recv, _method, rest)) =
1827            crate::generator::desugared_list_mutating_method(node, callee, args)
1828        else {
1829            return Ok(false);
1830        };
1831        let Some(x) = rest.first() else {
1832            return Ok(false);
1833        };
1834        let recv_str = self.expr_to_string(recv)?;
1835        let x = self.expr_to_string(&x.value)?;
1836        let _ = write!(self.buf, "({recv_str}).push(({x}))");
1837        Ok(true)
1838    }
1839
1840    /// Emit a DQ30 in-place `List` mutator
1841    /// (`pop`/`remove_at`/`insert`/`reverse`/`set`) to its Rust form.
1842    ///
1843    /// Recognised via [`crate::generator::desugared_list_inplace_mutator`].
1844    /// Bock `List[T]` lowers to `Vec<T>`, whose mutators are exactly the
1845    /// contract — idiomatic, zero wrappers:
1846    ///
1847    /// - `pop` → `(recv).pop()` — `Vec::pop` natively returns `Option<T>`
1848    ///   (`None` on empty), the same native Optional rep the `match` lowering
1849    ///   expects;
1850    /// - `remove_at(i)` → `(recv).remove(i as usize)` — `Vec::remove` panics
1851    ///   natively on an out-of-bounds index;
1852    /// - `insert(i, x)` → `(recv).insert(i as usize, x)` — `Vec::insert`
1853    ///   accepts `0..=len` and panics past it, the exact DQ30 range;
1854    /// - `reverse` → `(recv).reverse()`;
1855    /// - `set(i, x)` → `(recv)[i as usize] = x` — native index-assign, panics
1856    ///   on out-of-bounds.
1857    ///
1858    /// Abort-message reconciliation (DQ23 convention): the native panics carry
1859    /// the operation, index, and length (`removal index (is 5) should be < len
1860    /// (is 3)`, `index out of bounds: the len is 3 but the index is 5`), so no
1861    /// message wrapper is added — matching how Rust's native divide-by-zero
1862    /// panic was kept for §3.6. A negative Bock index wraps through `as usize`
1863    /// into a huge value, which the same native bounds checks reject (abort
1864    /// preserved; the printed index is the wrapped value). The ownership pass
1865    /// guarantees the receiver is a `mut` lvalue, so the `&mut self` borrows
1866    /// resolve against the `let mut` / `mut` parameter binding.
1867    fn try_emit_list_inplace_mutator(
1868        &mut self,
1869        node: &AIRNode,
1870        callee: &AIRNode,
1871        args: &[bock_air::AirArg],
1872    ) -> Result<bool, CodegenError> {
1873        let Some((recv, method, rest)) =
1874            crate::generator::desugared_list_inplace_mutator(node, callee, args)
1875        else {
1876            return Ok(false);
1877        };
1878        let recv_str = self.expr_to_string(recv)?;
1879        let code = match method {
1880            "pop" => format!("({recv_str}).pop()"),
1881            "remove_at" => {
1882                let Some(idx) = rest.first() else {
1883                    return Ok(false);
1884                };
1885                let i = self.expr_to_string(&idx.value)?;
1886                format!("({recv_str}).remove(({i}) as usize)")
1887            }
1888            "insert" => {
1889                let (Some(idx), Some(x)) = (rest.first(), rest.get(1)) else {
1890                    return Ok(false);
1891                };
1892                let i = self.expr_to_string(&idx.value)?;
1893                let x = self.expr_to_string(&x.value)?;
1894                format!("({recv_str}).insert(({i}) as usize, ({x}))")
1895            }
1896            "reverse" => format!("({recv_str}).reverse()"),
1897            "set" => {
1898                let (Some(idx), Some(x)) = (rest.first(), rest.get(1)) else {
1899                    return Ok(false);
1900                };
1901                let i = self.expr_to_string(&idx.value)?;
1902                let x = self.expr_to_string(&x.value)?;
1903                format!("({recv_str})[({i}) as usize] = ({x})")
1904            }
1905            _ => return Ok(false),
1906        };
1907        self.buf.push_str(&code);
1908        Ok(true)
1909    }
1910
1911    /// Emit a functional (closure-taking) `List` built-in method call to its
1912    /// Rust form.
1913    ///
1914    /// Recognised via [`crate::generator::desugared_list_functional_method`].
1915    /// The receiver is `.clone()`d (Bock lists have value semantics — the
1916    /// receiver var stays usable) and iterated by value: `map`/`flat_map`/`fold`/
1917    /// `for_each`/`reduce` drive `.clone().into_iter()`; the predicate combinators
1918    /// `filter`/`find`/`any`/`all` drive `.iter().cloned()` and adapt their `&T`
1919    /// item to the Bock closure's by-value `T` parameter via `__x.clone()`. The
1920    /// closure is captured *once* into `__f` so it is evaluated a single time
1921    /// (the desugared `recv.map(recv, cb)` shape the generic fall-through emits
1922    /// otherwise fails with `no method 'map' found for Vec`). `T: Clone` holds for
1923    /// every v1 element type (Int/Float/String/Bool and `#[derive(Clone)]`
1924    /// records); a generic-`List[T]` use is gated to synthesize the bound via
1925    /// [`Self::body_clones_collection_element`].
1926    fn try_emit_list_functional_method(
1927        &mut self,
1928        node: &AIRNode,
1929        callee: &AIRNode,
1930        args: &[bock_air::AirArg],
1931    ) -> Result<bool, CodegenError> {
1932        let Some((recv, method, rest)) =
1933            crate::generator::desugared_list_functional_method(node, callee, args)
1934        else {
1935            return Ok(false);
1936        };
1937        let recv_str = self.expr_to_string(recv)?;
1938        // The closure is emitted *inline* into the adapter rather than bound to a
1939        // `let __f` first: a closure stored in a `let` whose params are the
1940        // inferred-placeholder `|x: _|` cannot always back-infer its parameter
1941        // types from a later `into_iter().map(__f)` (`E0282`), whereas a closure
1942        // passed directly into the iterator adapter takes its parameter type from
1943        // the adapter's item type.
1944        let code = match method {
1945            "map" => {
1946                let Some(cb) = rest.first() else {
1947                    return Ok(false);
1948                };
1949                let f = self.expr_to_string(&cb.value)?;
1950                format!("({recv_str}).clone().into_iter().map({f}).collect::<Vec<_>>()")
1951            }
1952            "flat_map" => {
1953                let Some(cb) = rest.first() else {
1954                    return Ok(false);
1955                };
1956                let f = self.expr_to_string(&cb.value)?;
1957                format!("({recv_str}).clone().into_iter().flat_map({f}).collect::<Vec<_>>()")
1958            }
1959            "filter" => {
1960                let Some(cb) = rest.first() else {
1961                    return Ok(false);
1962                };
1963                let f = self.expr_to_string(&cb.value)?;
1964                // The Bock predicate takes `T` by value, but `Iterator::filter`
1965                // passes `&T` — and a closure literal cannot infer its parameter
1966                // type from an immediate application. So the predicate is flowed
1967                // *directly* into `.map(..)` (which pins its `T` param) to compute
1968                // a parallel `Vec<bool>`, then zipped with the elements and
1969                // filtered on the bool, then projected back to the elements.
1970                format!(
1971                    "{{ let __p: Vec<bool> = ({recv_str}).clone().into_iter().map({f}).collect(); \
1972                     ({recv_str}).iter().cloned().zip(__p).filter(|__t| __t.1).map(|__t| __t.0).collect::<Vec<_>>() }}"
1973                )
1974            }
1975            "find" => {
1976                let Some(cb) = rest.first() else {
1977                    return Ok(false);
1978                };
1979                let f = self.expr_to_string(&cb.value)?;
1980                // Same map-pinning approach as `filter`; `find` then returns the
1981                // first element whose paired predicate is true (`Option<T>`, the
1982                // Rust Optional rep).
1983                format!(
1984                    "{{ let __p: Vec<bool> = ({recv_str}).clone().into_iter().map({f}).collect(); \
1985                     ({recv_str}).iter().cloned().zip(__p).find(|__t| __t.1).map(|__t| __t.0) }}"
1986                )
1987            }
1988            "any" | "all" => {
1989                let Some(cb) = rest.first() else {
1990                    return Ok(false);
1991                };
1992                let f = self.expr_to_string(&cb.value)?;
1993                // Compute the predicate over each element via `.map(..)` (which
1994                // pins the closure's `T` param), then short-circuit with the
1995                // bool-iterator `any`/`all`.
1996                format!("({recv_str}).clone().into_iter().map({f}).{method}(|__b| __b)")
1997            }
1998            "reduce" => {
1999                let Some(cb) = rest.first() else {
2000                    return Ok(false);
2001                };
2002                let f = self.expr_to_string(&cb.value)?;
2003                // Bock `reduce` has no seed (first element is the accumulator) and
2004                // returns `T`; `Iterator::reduce` returns `Option<T>`.
2005                format!(
2006                    "({recv_str}).clone().into_iter().reduce({f}).expect(\"reduce on an empty list\")"
2007                )
2008            }
2009            "fold" => {
2010                let (Some(init), Some(cb)) = (rest.first(), rest.get(1)) else {
2011                    return Ok(false);
2012                };
2013                let init = self.expr_to_string(&init.value)?;
2014                let f = self.expr_to_string(&cb.value)?;
2015                format!("({recv_str}).clone().into_iter().fold({init}, {f})")
2016            }
2017            "for_each" => {
2018                let Some(cb) = rest.first() else {
2019                    return Ok(false);
2020                };
2021                let f = self.expr_to_string(&cb.value)?;
2022                format!("({recv_str}).clone().into_iter().for_each({f})")
2023            }
2024            _ => return Ok(false),
2025        };
2026        self.buf.push_str(&code);
2027        Ok(true)
2028    }
2029
2030    /// Emit a built-in `Map[K, V]` method call to its Rust form (native
2031    /// `std::collections::HashMap`).
2032    ///
2033    /// Recognised via [`crate::generator::desugared_map_method`] (gated on
2034    /// `recv_kind = "Map"`) and wired *before* [`Self::try_emit_list_method`],
2035    /// so a `Map` receiver's `get`/`contains_key`/`len` no longer route through
2036    /// the `List` path (where `get` cast the *key* to `usize` and indexed the
2037    /// map as a slice). `get` returns Rust's native `Option<V>` (`.get(&k)
2038    /// .cloned()`), the same rep the Rust `match` / Optional lowering expects.
2039    /// Mutating methods (`set`/`delete`/`merge`) clone-then-mutate and return
2040    /// the new map (Bock map value semantics; the receiver var need not be
2041    /// `mut`). `K: Hash + Eq` and `K, V: Clone` hold for the v1 element types.
2042    /// Returns `true` if handled.
2043    fn try_emit_map_method(
2044        &mut self,
2045        node: &AIRNode,
2046        callee: &AIRNode,
2047        args: &[bock_air::AirArg],
2048    ) -> Result<bool, CodegenError> {
2049        let Some((recv, method, rest)) = crate::generator::desugared_map_method(node, callee, args)
2050        else {
2051            return Ok(false);
2052        };
2053        let recv_str = self.expr_to_string(recv)?;
2054        let code = match method {
2055            "len" | "length" | "count" => format!("(({recv_str}).len() as i64)"),
2056            "is_empty" => format!("({recv_str}).is_empty()"),
2057            "contains_key" => {
2058                let Some(k) = rest.first() else {
2059                    return Ok(false);
2060                };
2061                let k = self.expr_to_string(&k.value)?;
2062                format!("({recv_str}).contains_key(&({k}))")
2063            }
2064            "get" => {
2065                let Some(k) = rest.first() else {
2066                    return Ok(false);
2067                };
2068                let k = self.expr_to_string(&k.value)?;
2069                format!("({recv_str}).get(&({k})).cloned()")
2070            }
2071            "set" => {
2072                let (Some(k), Some(v)) = (rest.first(), rest.get(1)) else {
2073                    return Ok(false);
2074                };
2075                let k = self.expr_to_string(&k.value)?;
2076                let v = self.expr_to_string(&v.value)?;
2077                format!("{{ let mut __m = ({recv_str}).clone(); __m.insert({k}, {v}); __m }}")
2078            }
2079            "delete" => {
2080                let Some(k) = rest.first() else {
2081                    return Ok(false);
2082                };
2083                let k = self.expr_to_string(&k.value)?;
2084                format!("{{ let mut __m = ({recv_str}).clone(); __m.remove(&({k})); __m }}")
2085            }
2086            "merge" => {
2087                let Some(o) = rest.first() else {
2088                    return Ok(false);
2089                };
2090                let o = self.expr_to_string(&o.value)?;
2091                format!("{{ let mut __m = ({recv_str}).clone(); __m.extend(({o}).clone()); __m }}")
2092            }
2093            "filter" => {
2094                let Some(f) = rest.first() else {
2095                    return Ok(false);
2096                };
2097                let f = self.expr_to_string(&f.value)?;
2098                format!(
2099                    "({recv_str}).iter().filter(|(__k, __v)| ({f})((*__k).clone(), (*__v).clone()))\
2100                     .map(|(__k, __v)| (__k.clone(), __v.clone()))\
2101                     .collect::<std::collections::HashMap<_, _>>()"
2102                )
2103            }
2104            "keys" => {
2105                format!("({recv_str}).keys().cloned().collect::<Vec<_>>()")
2106            }
2107            "values" => {
2108                format!("({recv_str}).values().cloned().collect::<Vec<_>>()")
2109            }
2110            "entries" | "to_list" => {
2111                format!(
2112                    "({recv_str}).iter().map(|(__k, __v)| (__k.clone(), __v.clone()))\
2113                     .collect::<Vec<_>>()"
2114                )
2115            }
2116            "for_each" => {
2117                let Some(f) = rest.first() else {
2118                    return Ok(false);
2119                };
2120                let f = self.expr_to_string(&f.value)?;
2121                format!(
2122                    "{{ for (__k, __v) in ({recv_str}).iter() {{ \
2123                     ({f})(__k.clone(), __v.clone()); }} }}"
2124                )
2125            }
2126            _ => return Ok(false),
2127        };
2128        self.buf.push_str(&code);
2129        Ok(true)
2130    }
2131
2132    /// Emit a built-in `Set[E]` method call to its Rust form (native
2133    /// `std::collections::HashSet`).
2134    ///
2135    /// Recognised via [`crate::generator::desugared_set_method`] (gated on
2136    /// `recv_kind = "Set"`) and wired *before* [`Self::try_emit_list_method`].
2137    /// Set algebra maps to the native `HashSet` combinators; `contains` is the
2138    /// native membership test (not the `List` linear scan). Mutating methods
2139    /// (`add`/`remove`) clone-then-mutate and return the new set. `E: Hash + Eq
2140    /// + Clone` holds for the v1 element types.
2141    fn try_emit_set_method(
2142        &mut self,
2143        node: &AIRNode,
2144        callee: &AIRNode,
2145        args: &[bock_air::AirArg],
2146    ) -> Result<bool, CodegenError> {
2147        let Some((recv, method, rest)) = crate::generator::desugared_set_method(node, callee, args)
2148        else {
2149            return Ok(false);
2150        };
2151        let recv_str = self.expr_to_string(recv)?;
2152        let code = match method {
2153            "len" | "length" | "count" => format!("(({recv_str}).len() as i64)"),
2154            "is_empty" => format!("({recv_str}).is_empty()"),
2155            "contains" => {
2156                let Some(x) = rest.first() else {
2157                    return Ok(false);
2158                };
2159                let x = self.expr_to_string(&x.value)?;
2160                format!("({recv_str}).contains(&({x}))")
2161            }
2162            "add" => {
2163                let Some(x) = rest.first() else {
2164                    return Ok(false);
2165                };
2166                let x = self.expr_to_string(&x.value)?;
2167                format!("{{ let mut __s = ({recv_str}).clone(); __s.insert({x}); __s }}")
2168            }
2169            "remove" => {
2170                let Some(x) = rest.first() else {
2171                    return Ok(false);
2172                };
2173                let x = self.expr_to_string(&x.value)?;
2174                format!("{{ let mut __s = ({recv_str}).clone(); __s.remove(&({x})); __s }}")
2175            }
2176            "union" => {
2177                let Some(o) = rest.first() else {
2178                    return Ok(false);
2179                };
2180                let o = self.expr_to_string(&o.value)?;
2181                format!(
2182                    "({recv_str}).union(&({o})).cloned().collect::<std::collections::HashSet<_>>()"
2183                )
2184            }
2185            "intersection" => {
2186                let Some(o) = rest.first() else {
2187                    return Ok(false);
2188                };
2189                let o = self.expr_to_string(&o.value)?;
2190                format!(
2191                    "({recv_str}).intersection(&({o})).cloned()\
2192                     .collect::<std::collections::HashSet<_>>()"
2193                )
2194            }
2195            "difference" => {
2196                let Some(o) = rest.first() else {
2197                    return Ok(false);
2198                };
2199                let o = self.expr_to_string(&o.value)?;
2200                format!(
2201                    "({recv_str}).difference(&({o})).cloned()\
2202                     .collect::<std::collections::HashSet<_>>()"
2203                )
2204            }
2205            "is_subset" => {
2206                let Some(o) = rest.first() else {
2207                    return Ok(false);
2208                };
2209                let o = self.expr_to_string(&o.value)?;
2210                format!("({recv_str}).is_subset(&({o}))")
2211            }
2212            "is_superset" => {
2213                let Some(o) = rest.first() else {
2214                    return Ok(false);
2215                };
2216                let o = self.expr_to_string(&o.value)?;
2217                format!("({recv_str}).is_superset(&({o}))")
2218            }
2219            "filter" => {
2220                let Some(f) = rest.first() else {
2221                    return Ok(false);
2222                };
2223                let f = self.expr_to_string(&f.value)?;
2224                format!(
2225                    "({recv_str}).iter().filter(|__x| ({f})((*__x).clone())).cloned()\
2226                     .collect::<std::collections::HashSet<_>>()"
2227                )
2228            }
2229            "map" => {
2230                let Some(f) = rest.first() else {
2231                    return Ok(false);
2232                };
2233                let f = self.expr_to_string(&f.value)?;
2234                format!(
2235                    "({recv_str}).iter().map(|__x| ({f})(__x.clone()))\
2236                     .collect::<std::collections::HashSet<_>>()"
2237                )
2238            }
2239            "to_list" => {
2240                format!("({recv_str}).iter().cloned().collect::<Vec<_>>()")
2241            }
2242            "for_each" => {
2243                let Some(f) = rest.first() else {
2244                    return Ok(false);
2245                };
2246                let f = self.expr_to_string(&f.value)?;
2247                format!("{{ for __x in ({recv_str}).iter() {{ ({f})(__x.clone()); }} }}")
2248            }
2249            _ => return Ok(false),
2250        };
2251        self.buf.push_str(&code);
2252        Ok(true)
2253    }
2254
2255    /// Lower a primitive trait-bridge method call (`compare`/`eq`/`to_string`/
2256    /// `display` on a primitive receiver) to its Rust intrinsic.
2257    ///
2258    /// `(1).compare(2)` resolves in the checker to `Ordering`, but `i64` has no
2259    /// `.compare` method; this maps it to `i64::cmp` (→ `std::cmp::Ordering`,
2260    /// which the construction/match sides also use). `compare` on a float maps
2261    /// to `partial_cmp(...).unwrap()` (floats are only `PartialOrd`). `eq`
2262    /// becomes `==`; `to_string`/`display` become `.to_string()`.
2263    /// Best-effort detection that `node` evaluates to a Rust `String` (or
2264    /// `&str`), used to route `+` to `format!` concat. Recognises the syntactic
2265    /// shapes that unambiguously produce a string: a string literal, a
2266    /// `format!`-lowered interpolation, and the desugared `String` built-in
2267    /// methods whose return type is `String` (`to_upper`/`to_lower`/`trim`/
2268    /// `replace`) or the `to_string`/`display` bridge. This is intentionally
2269    /// conservative — a false negative leaves a non-string `+` untouched (still
2270    /// correct for numbers); the recognised shapes cover the string-concat code
2271    /// that arises in practice. A nested `+` whose own operands are strings is
2272    /// itself a `String`, so the recursion threads through chained concat.
2273    fn expr_is_string_rs(node: &AIRNode) -> bool {
2274        match &node.kind {
2275            NodeKind::Literal {
2276                lit: Literal::String(_),
2277            } => true,
2278            NodeKind::Interpolation { .. } => true,
2279            NodeKind::BinaryOp {
2280                op: BinOp::Add,
2281                left,
2282                right,
2283            } => Self::expr_is_string_rs(left) || Self::expr_is_string_rs(right),
2284            NodeKind::Call { callee, .. } => {
2285                let NodeKind::FieldAccess { field, .. } = &callee.kind else {
2286                    return false;
2287                };
2288                matches!(
2289                    field.name.as_str(),
2290                    "to_upper" | "to_lower" | "trim" | "replace" | "to_string" | "display"
2291                )
2292            }
2293            _ => false,
2294        }
2295    }
2296
2297    /// Lower a desugared `String` built-in method call (`recv_kind =
2298    /// "Primitive:String"`) to its native Rust string op. Wired into the `Call`
2299    /// arm *before* `try_emit_list_method` so a String receiver's
2300    /// `len`/`contains`/`is_empty` dispatch here, not through the List path.
2301    ///
2302    /// `len` is the Unicode SCALAR count (`(s).chars().count() as i64`) per spec
2303    /// §18.3 — Rust's `str::len` is the BYTE length, so `byte_len` maps to it
2304    /// (`(s).len() as i64`). String args (literals emit as owned `String`) are
2305    /// passed by reference (`&(..)`), which derefs to the `&str`/`Pattern` the
2306    /// `str` methods expect. `replace` replaces ALL occurrences (Rust's default).
2307    /// `split` collects to a `Vec<String>`, the List runtime rep.
2308    fn try_emit_string_method(
2309        &mut self,
2310        node: &AIRNode,
2311        callee: &AIRNode,
2312        args: &[bock_air::AirArg],
2313    ) -> Result<bool, CodegenError> {
2314        // Gate on the checker's `recv_kind = "Primitive:String"` stamp directly
2315        // (rather than [`crate::generator::desugared_string_method`], which only
2316        // admits the cross-backend `STRING_METHODS` subset). Rust can lower a
2317        // wider set — `slice`/`substring`/`char_at`/`index_of`/`repeat`/`reverse`
2318        // — to native `str` ops, so it recognises the full resolved String method
2319        // surface here without widening the shared const (which would force the
2320        // other backends to handle the extra names too).
2321        if crate::generator::primitive_recv_kind(node) != Some("String") {
2322            return Ok(false);
2323        }
2324        let Some((recv, field, rest)) = crate::generator::desugared_self_call(callee, args) else {
2325            return Ok(false);
2326        };
2327        let method = field.name.as_str();
2328        let recv_str = self.expr_to_string(recv)?;
2329        let arg0 = |this: &mut Self| -> Result<Option<String>, CodegenError> {
2330            rest.first()
2331                .map(|a| this.expr_to_string(&a.value))
2332                .transpose()
2333        };
2334        let code = match method {
2335            "len" | "length" | "count" => format!("(({recv_str}).chars().count() as i64)"),
2336            "byte_len" => format!("(({recv_str}).len() as i64)"),
2337            "is_empty" => format!("({recv_str}).is_empty()"),
2338            "to_upper" => format!("({recv_str}).to_uppercase()"),
2339            "to_lower" => format!("({recv_str}).to_lowercase()"),
2340            "trim" => format!("({recv_str}).trim().to_string()"),
2341            "trim_start" => format!("({recv_str}).trim_start().to_string()"),
2342            "trim_end" => format!("({recv_str}).trim_end().to_string()"),
2343            "reverse" => format!("({recv_str}).chars().rev().collect::<String>()"),
2344            "to_string" | "display" => format!("({recv_str}).to_string()"),
2345            "repeat" => {
2346                let Some(n) = arg0(self)? else {
2347                    return Ok(false);
2348                };
2349                format!("({recv_str}).repeat(({n}) as usize)")
2350            }
2351            "contains" => {
2352                let Some(p) = arg0(self)? else {
2353                    return Ok(false);
2354                };
2355                format!("({recv_str}).contains(&({p}) as &str)")
2356            }
2357            "starts_with" => {
2358                let Some(p) = arg0(self)? else {
2359                    return Ok(false);
2360                };
2361                format!("({recv_str}).starts_with(&({p}) as &str)")
2362            }
2363            "ends_with" => {
2364                let Some(p) = arg0(self)? else {
2365                    return Ok(false);
2366                };
2367                format!("({recv_str}).ends_with(&({p}) as &str)")
2368            }
2369            "replace" => {
2370                let Some(from) = arg0(self)? else {
2371                    return Ok(false);
2372                };
2373                let Some(to) = rest
2374                    .get(1)
2375                    .map(|a| self.expr_to_string(&a.value))
2376                    .transpose()?
2377                else {
2378                    return Ok(false);
2379                };
2380                format!("({recv_str}).replace(&({from}) as &str, &({to}) as &str)")
2381            }
2382            "split" => {
2383                let Some(sep) = arg0(self)? else {
2384                    return Ok(false);
2385                };
2386                format!(
2387                    "({recv_str}).split(&({sep}) as &str).map(|__p| __p.to_string()).collect::<Vec<String>>()"
2388                )
2389            }
2390            // `slice`/`substring(start, end)` are scalar-index half-open
2391            // substrings (spec §18.3 — `len` is the Unicode scalar count, so
2392            // indices are scalar positions, not bytes). Lowered via a char
2393            // iterator so multibyte input is handled correctly and the result is
2394            // an owned `String`. `start`/`end` are clamped by `take`'s saturating
2395            // subtraction (`end.saturating_sub(start)`).
2396            "slice" | "substring" => {
2397                let Some(start) = arg0(self)? else {
2398                    return Ok(false);
2399                };
2400                let Some(end) = rest
2401                    .get(1)
2402                    .map(|a| self.expr_to_string(&a.value))
2403                    .transpose()?
2404                else {
2405                    return Ok(false);
2406                };
2407                format!(
2408                    "({recv_str}).chars().skip(({start}) as usize).take((({end}) as i64 - ({start}) as i64).max(0) as usize).collect::<String>()"
2409                )
2410            }
2411            // `char_at(i)` returns `Optional[Char]` — `None` when out of range.
2412            "char_at" => {
2413                let Some(i) = arg0(self)? else {
2414                    return Ok(false);
2415                };
2416                format!("({recv_str}).chars().nth(({i}) as usize)")
2417            }
2418            // `index_of(needle)` returns `Optional[Int]` — the scalar index of the
2419            // first match, or `None`. Rust's `str::find` yields a *byte* offset,
2420            // so convert it to a scalar index via the char-boundary count.
2421            "index_of" => {
2422                let Some(p) = arg0(self)? else {
2423                    return Ok(false);
2424                };
2425                format!(
2426                    "({recv_str}).find(&({p}) as &str).map(|__b| ({recv_str})[..__b].chars().count() as i64)"
2427                )
2428            }
2429            _ => return Ok(false),
2430        };
2431        self.buf.push_str(&code);
2432        Ok(true)
2433    }
2434
2435    /// Q-prim-assoc: lower a primitive associated-conversion call
2436    /// (`Float.from(x)` / `Int.try_from(s)` / `String.from(c)`) to Rust's native
2437    /// conversion. `from` is an `as` cast (`x as f64` / `x as i64`; `f64::from`
2438    /// has no `i64` impl) or `char::to_string`. `try_from` parses with
2439    /// `str::parse` inside a `match` expression that maps `Ok`/`Err` to the Bock
2440    /// `Result` (native `Result<T, ConvertError>` on Rust), the `Err` payload a
2441    /// `ConvertError` struct literal (in scope via the `Result[T, ConvertError]`
2442    /// return-type import). Returns `true` when handled.
2443    fn try_emit_primitive_conversion(
2444        &mut self,
2445        node: &AIRNode,
2446        callee: &AIRNode,
2447        args: &[bock_air::AirArg],
2448    ) -> Result<bool, CodegenError> {
2449        let Some((target, method, arg)) =
2450            crate::generator::primitive_conversion_call(node, callee, args)
2451        else {
2452            return Ok(false);
2453        };
2454        let arg_str = self.expr_to_string(arg)?;
2455        let code = match (target, method) {
2456            // Widening casts: `i64 as f64` / sized-int `as i64`. `as` is the
2457            // only spelling that covers i64 -> f64 (no `f64::from(i64)`).
2458            ("Float", "from") => format!("(({arg_str}) as f64)"),
2459            ("Int", "from") => format!("(({arg_str}) as i64)"),
2460            // Char -> String. A Bock `Char` is a Rust `char`.
2461            ("String", "from") => format!("(({arg_str}).to_string())"),
2462            ("Int", "try_from") => format!(
2463                "(match ({arg_str}).trim().parse::<i64>() {{ \
2464                 Ok(__v) => Ok(__v), \
2465                 Err(_) => Err(ConvertError {{ message: format!(\"cannot parse {{:?}} as Int\", ({arg_str})) }}) }})"
2466            ),
2467            ("Float", "try_from") => format!(
2468                "(match ({arg_str}).trim().parse::<f64>() {{ \
2469                 Ok(__v) => Ok(__v), \
2470                 Err(_) => Err(ConvertError {{ message: format!(\"cannot parse {{:?}} as Float\", ({arg_str})) }}) }})"
2471            ),
2472            _ => return Ok(false),
2473        };
2474        self.buf.push_str(&code);
2475        Ok(true)
2476    }
2477
2478    /// Lower a desugared numeric/`Char`/`Bool` primitive method (`recv_kind =
2479    /// "Primitive:Int" | "Primitive:Float" | "Primitive:Char" | "Primitive:Bool"`)
2480    /// to its native Rust form. Covers the conversion and math methods the checker
2481    /// resolves on the scalar primitives — `to_float`/`to_int`/`abs`/`min`/`max`/
2482    /// `clamp`/`floor`/`ceil`/`round`/`sqrt`/… — none of which exist as inherent
2483    /// methods on `i64`/`f64` in Rust. Wired into the `Call` arm alongside
2484    /// [`Self::try_emit_string_method`], before the generic desugared-self-call
2485    /// fall-through (which would emit `n.to_float(n)`, undefined on `i64`).
2486    /// `compare`/`eq`/`to_string`/`display` stay on the primitive *bridge* path.
2487    fn try_emit_numeric_method(
2488        &mut self,
2489        node: &AIRNode,
2490        callee: &AIRNode,
2491        args: &[bock_air::AirArg],
2492    ) -> Result<bool, CodegenError> {
2493        let prim = match crate::generator::primitive_recv_kind(node) {
2494            Some(p @ ("Int" | "Float" | "Char" | "Bool")) => p,
2495            _ => return Ok(false),
2496        };
2497        let Some((recv, field, rest)) = crate::generator::desugared_self_call(callee, args) else {
2498            return Ok(false);
2499        };
2500        let method = field.name.as_str();
2501        let recv_str = self.expr_to_string(recv)?;
2502        let arg = |this: &mut Self, i: usize| -> Result<Option<String>, CodegenError> {
2503            rest.get(i)
2504                .map(|a| this.expr_to_string(&a.value))
2505                .transpose()
2506        };
2507        let code = match (prim, method) {
2508            // Int → Float / Float → Int conversions.
2509            ("Int", "to_float") => format!("(({recv_str}) as f64)"),
2510            ("Float", "to_int") => format!("(({recv_str}) as i64)"),
2511            // `Char.to_int` is the scalar value; `Bool.to_int` is 0/1.
2512            ("Char", "to_int") => format!("(({recv_str}) as i64)"),
2513            ("Bool", "to_int") => format!("(if ({recv_str}) {{ 1i64 }} else {{ 0i64 }})"),
2514            // Int math.
2515            ("Int", "abs") => format!("({recv_str}).abs()"),
2516            ("Int", "min") => {
2517                let Some(o) = arg(self, 0)? else {
2518                    return Ok(false);
2519                };
2520                format!("({recv_str}).min({o})")
2521            }
2522            ("Int", "max") => {
2523                let Some(o) = arg(self, 0)? else {
2524                    return Ok(false);
2525                };
2526                format!("({recv_str}).max({o})")
2527            }
2528            ("Int", "clamp") => {
2529                let (Some(lo), Some(hi)) = (arg(self, 0)?, arg(self, 1)?) else {
2530                    return Ok(false);
2531                };
2532                format!("({recv_str}).clamp({lo}, {hi})")
2533            }
2534            ("Int", "shift_left") => {
2535                let Some(o) = arg(self, 0)? else {
2536                    return Ok(false);
2537                };
2538                format!("(({recv_str}) << ({o}))")
2539            }
2540            ("Int", "shift_right") => {
2541                let Some(o) = arg(self, 0)? else {
2542                    return Ok(false);
2543                };
2544                format!("(({recv_str}) >> ({o}))")
2545            }
2546            // Float math.
2547            ("Float", "abs") => format!("({recv_str}).abs()"),
2548            ("Float", "floor") => format!("({recv_str}).floor()"),
2549            ("Float", "ceil") => format!("({recv_str}).ceil()"),
2550            ("Float", "round") => format!("({recv_str}).round()"),
2551            ("Float", "sqrt") => format!("({recv_str}).sqrt()"),
2552            ("Float", "is_nan") => format!("({recv_str}).is_nan()"),
2553            ("Float", "is_infinite") => format!("({recv_str}).is_infinite()"),
2554            ("Float", "min") => {
2555                let Some(o) = arg(self, 0)? else {
2556                    return Ok(false);
2557                };
2558                format!("({recv_str}).min({o})")
2559            }
2560            ("Float", "max") => {
2561                let Some(o) = arg(self, 0)? else {
2562                    return Ok(false);
2563                };
2564                format!("({recv_str}).max({o})")
2565            }
2566            ("Float", "clamp") => {
2567                let (Some(lo), Some(hi)) = (arg(self, 0)?, arg(self, 1)?) else {
2568                    return Ok(false);
2569                };
2570                format!("({recv_str}).clamp({lo}, {hi})")
2571            }
2572            // Bool.
2573            ("Bool", "negate") => format!("(!({recv_str}))"),
2574            // Char.
2575            ("Char", "to_upper") => {
2576                format!("({recv_str}).to_uppercase().next().unwrap_or({recv_str})")
2577            }
2578            ("Char", "to_lower") => {
2579                format!("({recv_str}).to_lowercase().next().unwrap_or({recv_str})")
2580            }
2581            ("Char", "is_alpha") => format!("({recv_str}).is_alphabetic()"),
2582            ("Char", "is_digit") => format!("({recv_str}).is_ascii_digit()"),
2583            ("Char", "is_whitespace") => format!("({recv_str}).is_whitespace()"),
2584            _ => return Ok(false),
2585        };
2586        self.buf.push_str(&code);
2587        Ok(true)
2588    }
2589
2590    fn try_emit_primitive_bridge(
2591        &mut self,
2592        node: &AIRNode,
2593        callee: &AIRNode,
2594        args: &[bock_air::AirArg],
2595    ) -> Result<bool, CodegenError> {
2596        let Some((recv, method, rest, prim)) =
2597            crate::generator::primitive_bridge_call(node, callee, args)
2598        else {
2599            return Ok(false);
2600        };
2601        // Floats are only `PartialOrd` in Rust; everything else is `Ord`.
2602        let partial = prim.starts_with("Float") || prim == "BigFloat" || prim == "Decimal";
2603        self.emit_bridge_method(recv, method, rest, partial)
2604    }
2605
2606    /// Lower a sealed-core-trait bridge method on a *bounded generic type
2607    /// variable* (`a.eq(b)` / `a.compare(b)` / `a.to_string()` inside
2608    /// `eq_check[T: Equatable]`) to its Rust intrinsic. The generic analogue of
2609    /// [`Self::try_emit_primitive_bridge`] (GAP-C): the receiver is `T`, whose
2610    /// sealed-core bound is rewritten to the matching std trait
2611    /// (`Equatable`→`PartialEq`, `Comparable`→`Ord`, `Displayable`→`Display`) at
2612    /// the signature, so `==` / `.cmp(&…)` / `.to_string()` type-check. Only fires
2613    /// when the bound trait is sealed-core and NOT a user-declared trait (a user
2614    /// trait provides the method through its own `impl`).
2615    fn try_emit_trait_bound_bridge(
2616        &mut self,
2617        node: &AIRNode,
2618        callee: &AIRNode,
2619        args: &[bock_air::AirArg],
2620    ) -> Result<bool, CodegenError> {
2621        let Some((recv, method, rest, _tr)) =
2622            crate::generator::trait_bound_bridge_call(node, callee, args, &self.trait_decls)
2623        else {
2624            return Ok(false);
2625        };
2626        // A generic `T: Ord` always uses the total-order `.cmp`; there is no
2627        // partial-order generic bound (a `Float`-only bound is not expressible).
2628        self.emit_bridge_method(recv, method, rest, false)
2629    }
2630
2631    /// Shared body of the primitive / trait-bound bridges: emit the native Rust
2632    /// form of `compare` (`Ordering` via `.cmp`/`.partial_cmp`), `eq` (`==`), or
2633    /// `to_string`/`display` (`.to_string()`) for the receiver + remaining args.
2634    /// `partial` selects `.partial_cmp(..).unwrap()` over `.cmp(..)` for the
2635    /// `PartialOrd`-only float types.
2636    fn emit_bridge_method(
2637        &mut self,
2638        recv: &AIRNode,
2639        method: &str,
2640        rest: &[bock_air::AirArg],
2641        partial: bool,
2642    ) -> Result<bool, CodegenError> {
2643        let recv_str = self.expr_to_string(recv)?;
2644        let code = match method {
2645            "compare" => {
2646                let Some(other) = rest.first() else {
2647                    return Ok(false);
2648                };
2649                let other = self.expr_to_string(&other.value)?;
2650                if partial {
2651                    format!("({recv_str}).partial_cmp(&({other})).unwrap()")
2652                } else {
2653                    format!("({recv_str}).cmp(&({other}))")
2654                }
2655            }
2656            "eq" => {
2657                let Some(other) = rest.first() else {
2658                    return Ok(false);
2659                };
2660                let other = self.expr_to_string(&other.value)?;
2661                format!("(({recv_str}) == ({other}))")
2662            }
2663            "to_string" | "display" => format!("({recv_str}).to_string()"),
2664            _ => return Ok(false),
2665        };
2666        self.buf.push_str(&code);
2667        Ok(true)
2668    }
2669
2670    /// Recognise `Duration.xxx(...)` / `Instant.xxx(...)` associated-function
2671    /// calls and emit equivalent Rust `std::time` usage. Durations are i64
2672    /// nanoseconds; Instants are `std::time::Instant`.
2673    fn try_emit_time_assoc_call(
2674        &mut self,
2675        callee: &AIRNode,
2676        args: &[bock_air::AirArg],
2677    ) -> Result<bool, CodegenError> {
2678        let NodeKind::FieldAccess { object, field } = &callee.kind else {
2679            return Ok(false);
2680        };
2681        let NodeKind::Identifier { name: type_name } = &object.kind else {
2682            return Ok(false);
2683        };
2684        let arg_strs: Vec<String> = args
2685            .iter()
2686            .map(|a| self.expr_to_string(&a.value))
2687            .collect::<Result<_, _>>()?;
2688        let arg0 = || arg_strs.first().cloned().unwrap_or_default();
2689        let code = match (type_name.name.as_str(), field.name.as_str()) {
2690            ("Duration", "zero") => "0i64".to_string(),
2691            ("Duration", "nanos") => format!("(({}) as i64)", arg0()),
2692            ("Duration", "micros") => format!("((({}) as i64) * 1_000)", arg0()),
2693            ("Duration", "millis") => format!("((({}) as i64) * 1_000_000)", arg0()),
2694            ("Duration", "seconds") => format!("((({}) as i64) * 1_000_000_000)", arg0()),
2695            ("Duration", "minutes") => format!("((({}) as i64) * 60_000_000_000)", arg0()),
2696            ("Duration", "hours") => format!("((({}) as i64) * 3_600_000_000_000)", arg0()),
2697            ("Instant", "now") => {
2698                // Route through an installed `Clock` handler's `now_monotonic`
2699                // op if one is in scope; otherwise emit the host primitive.
2700                if let Some(handler) = self.clock_handler_var() {
2701                    format!("{handler}.{}()", to_snake_case("now_monotonic"))
2702                } else {
2703                    "std::time::Instant::now()".to_string()
2704                }
2705            }
2706            _ => return Ok(false),
2707        };
2708        self.buf.push_str(&code);
2709        Ok(true)
2710    }
2711
2712    /// Recognise desugared method calls on Duration/Instant values.
2713    fn try_emit_time_desugared_method(
2714        &mut self,
2715        callee: &AIRNode,
2716        args: &[bock_air::AirArg],
2717    ) -> Result<bool, CodegenError> {
2718        let NodeKind::FieldAccess { object, field } = &callee.kind else {
2719            return Ok(false);
2720        };
2721        if let NodeKind::Identifier { name } = &object.kind {
2722            if matches!(name.name.as_str(), "Duration" | "Instant") {
2723                return Ok(false);
2724            }
2725        }
2726        if !is_time_method_name(&field.name) {
2727            return Ok(false);
2728        }
2729        let remaining: Vec<bock_air::AirArg> = args.iter().skip(1).cloned().collect();
2730        self.try_emit_time_method(object, &field.name, &remaining)
2731    }
2732
2733    /// Recognise `Channel.new()`, `spawn(...)`, and method calls on a
2734    /// channel value. Emits the Rust runtime helper equivalents using
2735    /// `tokio::sync::mpsc` under the hood.
2736    fn try_emit_concurrency_call(
2737        &mut self,
2738        callee: &AIRNode,
2739        args: &[bock_air::AirArg],
2740    ) -> Result<bool, CodegenError> {
2741        if let NodeKind::Identifier { name } = &callee.kind {
2742            if name.name == "spawn" {
2743                // spawn(x) — x is expected to be an async fn invocation
2744                // (a Future) in Bock. In Rust we wrap it in `async move`
2745                // so tokio::spawn can take ownership.
2746                self.buf.push_str("__bock_spawn(async move { ");
2747                for (i, arg) in args.iter().enumerate() {
2748                    if i > 0 {
2749                        self.buf.push_str(", ");
2750                    }
2751                    self.emit_expr(&arg.value)?;
2752                    self.buf.push_str(".await");
2753                }
2754                self.buf.push_str(" })");
2755                return Ok(true);
2756            }
2757        }
2758        let NodeKind::FieldAccess { object, field } = &callee.kind else {
2759            return Ok(false);
2760        };
2761        if let NodeKind::Identifier { name: type_name } = &object.kind {
2762            if type_name.name == "Channel" && field.name == "new" {
2763                self.buf.push_str("__bock_channel_new()");
2764                return Ok(true);
2765            }
2766        }
2767        if matches!(field.name.as_str(), "send" | "recv" | "close") {
2768            self.emit_expr(object)?;
2769            let _ = write!(self.buf, ".{}", field.name);
2770            self.buf.push('(');
2771            for (i, arg) in args.iter().skip(1).enumerate() {
2772                if i > 0 {
2773                    self.buf.push_str(", ");
2774                }
2775                self.emit_expr(&arg.value)?;
2776            }
2777            self.buf.push(')');
2778            return Ok(true);
2779        }
2780        Ok(false)
2781    }
2782
2783    /// Recognise instance methods on Duration/Instant values.
2784    fn try_emit_time_method(
2785        &mut self,
2786        receiver: &AIRNode,
2787        method: &str,
2788        args: &[bock_air::AirArg],
2789    ) -> Result<bool, CodegenError> {
2790        let recv_str = self.expr_to_string(receiver)?;
2791        let arg_strs: Vec<String> = args
2792            .iter()
2793            .map(|a| self.expr_to_string(&a.value))
2794            .collect::<Result<_, _>>()?;
2795        let code = match method {
2796            "as_nanos" => format!("({recv_str})"),
2797            "as_millis" => format!("(({recv_str}) / 1_000_000)"),
2798            "as_seconds" => format!("(({recv_str}) / 1_000_000_000)"),
2799            "is_zero" => format!("(({recv_str}) == 0)"),
2800            "is_negative" => format!("(({recv_str}) < 0)"),
2801            "abs" => format!("(({recv_str}) as i64).abs()"),
2802            "elapsed" => {
2803                // `instant.elapsed()` is derived: time-since-`recv`. Route the
2804                // "now" read through an installed `Clock` handler if in scope —
2805                // `now_monotonic()` yields a `std::time::Instant`, so the span is
2806                // `now.duration_since(recv)` as nanoseconds; otherwise read the
2807                // host monotonic clock via `recv.elapsed()` (default).
2808                if let Some(handler) = self.clock_handler_var() {
2809                    format!(
2810                        "({handler}.{}().duration_since({recv_str}).as_nanos() as i64)",
2811                        to_snake_case("now_monotonic")
2812                    )
2813                } else {
2814                    format!("(({recv_str}).elapsed().as_nanos() as i64)")
2815                }
2816            }
2817            "duration_since" => {
2818                let other = arg_strs.first().cloned().unwrap_or_default();
2819                format!("((({recv_str}).saturating_duration_since({other})).as_nanos() as i64)")
2820            }
2821            _ => return Ok(false),
2822        };
2823        self.buf.push_str(&code);
2824        Ok(true)
2825    }
2826
2827    // ── Type emission ────────────────────────────────────────────────────────
2828
2829    /// Emit an AIR type node to a Rust type string.
2830    /// Render a type that appears in a function signature's *param* or *return*
2831    /// position. A Bock `Fn(A) -> B` value there is lowered to `impl Fn(A) -> B`
2832    /// rather than the bare `fn(A) -> B` pointer: a Bock closure may capture
2833    /// (`(x) => f(g(x))` capturing `f`/`g`, the `>>`-compose and curried-call
2834    /// shapes), and a capturing closure does not coerce to a `fn` pointer
2835    /// (E0308). `impl Fn` accepts both fn-items and capturing closures. Only the
2836    /// outermost function type is widened — a nested `Fn` (e.g. a `Fn` returning
2837    /// a `Fn`) keeps the pointer form, which is rare and still coerces for the
2838    /// non-capturing case. Type *aliases* keep the `fn` pointer via
2839    /// [`Self::type_to_rs`]: `impl Trait` is not nameable in a `type` alias.
2840    fn type_to_rs_fn_pos(&mut self, node: &AIRNode) -> String {
2841        self.type_to_rs_fn_pos_bounded(node, false)
2842    }
2843
2844    /// As [`Self::type_to_rs_fn_pos`], but when `static_bound` is set an `impl
2845    /// Fn` lowering gains `+ 'static`. Used for the params of a function that
2846    /// *returns* a closure (see [`Self::returning_fn_closure`]): the moved
2847    /// captures must be `'static` to satisfy the returned `impl Fn` (E0310).
2848    fn type_to_rs_fn_pos_bounded(&mut self, node: &AIRNode, static_bound: bool) -> String {
2849        // Resolve through a `Fn`-typed `type` alias (`type EventHandler = Fn() ->
2850        // Void`): a signature position naming the alias lowers to `impl Fn(..)`
2851        // just like a literal `Fn(..)`, so a *capturing* closure flowing through
2852        // it type-checks (a `fn` pointer would reject it — E0308). The alias's
2853        // own `type` declaration keeps the `fn`-pointer form.
2854        let resolved = self.resolve_fn_closure_type(node).cloned();
2855        if let Some(NodeKind::TypeFunction { params, ret, .. }) = resolved.as_ref().map(|n| &n.kind)
2856        {
2857            let param_strs: Vec<String> = params.iter().map(|p| self.type_to_rs(p)).collect();
2858            let bound = if static_bound { " + 'static" } else { "" };
2859            format!(
2860                "impl Fn({}) -> {}{bound}",
2861                param_strs.join(", "),
2862                self.type_to_rs(ret)
2863            )
2864        } else {
2865            self.type_to_rs(node)
2866        }
2867    }
2868
2869    fn type_to_rs(&mut self, node: &AIRNode) -> String {
2870        match &node.kind {
2871            NodeKind::TypeNamed { path, args } => {
2872                let name = path
2873                    .segments
2874                    .iter()
2875                    .map(|s| s.name.as_str())
2876                    .collect::<Vec<_>>()
2877                    .join("::");
2878                let rs_name = self.map_type_name(&name);
2879                if args.is_empty() {
2880                    rs_name
2881                } else {
2882                    let arg_strs: Vec<String> = args.iter().map(|a| self.type_to_rs(a)).collect();
2883                    format!("{rs_name}<{}>", arg_strs.join(", "))
2884                }
2885            }
2886            NodeKind::TypeTuple { elems } => {
2887                let elem_strs: Vec<String> = elems.iter().map(|e| self.type_to_rs(e)).collect();
2888                format!("({})", elem_strs.join(", "))
2889            }
2890            NodeKind::TypeFunction { params, ret, .. } => {
2891                let param_strs: Vec<String> = params.iter().map(|p| self.type_to_rs(p)).collect();
2892                format!("fn({}) -> {}", param_strs.join(", "), self.type_to_rs(ret))
2893            }
2894            NodeKind::TypeOptional { inner } => {
2895                format!("Option<{}>", self.type_to_rs(inner))
2896            }
2897            NodeKind::TypeSelf => "Self".into(),
2898            _ => "_".into(),
2899        }
2900    }
2901
2902    /// Map Bock type names to Rust equivalents.
2903    fn map_type_name(&mut self, name: &str) -> String {
2904        match name {
2905            "Int" => "i64".into(),
2906            "Float" => "f64".into(),
2907            "Bool" => "bool".into(),
2908            "Char" => "char".into(),
2909            "String" => "String".into(),
2910            "Void" | "Unit" => "()".into(),
2911            "List" => "Vec".into(),
2912            "Map" => "std::collections::HashMap".into(),
2913            "Set" => "std::collections::HashSet".into(),
2914            "Any" => "Box<dyn std::any::Any>".into(),
2915            "Never" => "!".into(),
2916            "Optional" => "Option".into(),
2917            "Rc" => {
2918                self.needs_rc_import = true;
2919                "Rc".into()
2920            }
2921            "Arc" => {
2922                self.needs_arc_import = true;
2923                "Arc".into()
2924            }
2925            // §18.3.1 builtin time types: a `Duration` value lowers to a
2926            // signed-nanosecond `i64`, and an `Instant` to `std::time::Instant`
2927            // (`std::time::Instant::now()`). They are NOT user-defined types, so
2928            // as annotations (e.g. on a `Clock` handler's `now_monotonic() ->
2929            // Instant` / `sleep(duration: Duration)`) they must render their
2930            // concrete Rust forms, not the undefined identifiers.
2931            "Duration" => "i64".into(),
2932            "Instant" => "std::time::Instant".into(),
2933            // The prelude `Ordering` enum: when the real `core.compare.Ordering`
2934            // is NOT reachable (no `use core.compare`), its variants already
2935            // lower to the `std::cmp::Ordering` bridge (see the `Identifier`
2936            // arm), so the *type* annotation must agree — `fn compare(..) ->
2937            // Ordering` becomes `-> std::cmp::Ordering`. Without this the
2938            // annotation emitted the bare `Ordering` (E0425, undefined type) and
2939            // even with the enum reachable a body `.cmp()` (std Ordering)
2940            // mismatched a user-`Ordering` return (E0308). When the enum IS
2941            // reachable the user `enum Ordering` is in scope and keeps its name.
2942            // (Q-prelude-impl-missing-import.)
2943            "Ordering" if !self.ordering_enum_reachable() => "std::cmp::Ordering".into(),
2944            other => other.into(),
2945        }
2946    }
2947
2948    /// Emit an AST TypeExpr to a Rust type string (for record fields, etc.).
2949    fn ast_type_to_rs(&mut self, ty: &TypeExpr) -> String {
2950        match ty {
2951            TypeExpr::Named { path, args, .. } => {
2952                let name = path
2953                    .segments
2954                    .iter()
2955                    .map(|s| s.name.as_str())
2956                    .collect::<Vec<_>>()
2957                    .join("::");
2958                let rs_name = self.map_type_name(&name);
2959                if args.is_empty() {
2960                    rs_name
2961                } else {
2962                    let arg_strs: Vec<String> =
2963                        args.iter().map(|a| self.ast_type_to_rs(a)).collect();
2964                    format!("{rs_name}<{}>", arg_strs.join(", "))
2965                }
2966            }
2967            TypeExpr::Tuple { elems, .. } => {
2968                let elem_strs: Vec<String> = elems.iter().map(|e| self.ast_type_to_rs(e)).collect();
2969                format!("({})", elem_strs.join(", "))
2970            }
2971            TypeExpr::Function { params, ret, .. } => {
2972                let param_strs: Vec<String> =
2973                    params.iter().map(|p| self.ast_type_to_rs(p)).collect();
2974                format!(
2975                    "fn({}) -> {}",
2976                    param_strs.join(", "),
2977                    self.ast_type_to_rs(ret)
2978                )
2979            }
2980            TypeExpr::Optional { inner, .. } => {
2981                format!("Option<{}>", self.ast_type_to_rs(inner))
2982            }
2983            TypeExpr::SelfType { .. } => "Self".into(),
2984        }
2985    }
2986
2987    /// Emit generic parameter list: `<T, U: Foo>`.
2988    /// Render a *use-site* generic argument list (`<T>`, `<T, U>`) — bare param
2989    /// names, no bounds — for a type reference like `Box<T>`. Empty for none.
2990    fn generic_param_args_rs(&self, params: &[bock_ast::GenericParam]) -> String {
2991        if params.is_empty() {
2992            return String::new();
2993        }
2994        let names: Vec<&str> = params.iter().map(|p| p.name.name.as_str()).collect();
2995        format!("<{}>", names.join(", "))
2996    }
2997
2998    /// Render an impl's generic-param declaration, optionally adding a `Clone`
2999    /// bound to every param. Used for a generic clone-target impl whose method
3000    /// returns `self.field` by value (the field read clones, so `T: Clone`).
3001    fn generic_params_to_rs_with_clone(
3002        &self,
3003        params: &[bock_ast::GenericParam],
3004        add_clone: bool,
3005    ) -> String {
3006        if params.is_empty() {
3007            return String::new();
3008        }
3009        let items: Vec<String> = params
3010            .iter()
3011            .map(|p| {
3012                let mut bounds: Vec<String> = p
3013                    .bounds
3014                    .iter()
3015                    .map(|b| self.rs_bound_to_string(b))
3016                    .collect();
3017                if add_clone && !bounds.iter().any(|b| b == "Clone") {
3018                    bounds.push("Clone".to_string());
3019                }
3020                if bounds.is_empty() {
3021                    p.name.name.clone()
3022                } else {
3023                    format!("{}: {}", p.name.name, bounds.join(" + "))
3024                }
3025            })
3026            .collect();
3027        format!("<{}>", items.join(", "))
3028    }
3029
3030    /// The bare name of a named type expression (`Box` for `Box[T]`), dropping
3031    /// any generic arguments. Used to look a target up in the generic-decl
3032    /// registry, which is keyed by the undecorated declaration name.
3033    fn type_expr_base_name(&self, node: &AIRNode) -> String {
3034        match &node.kind {
3035            NodeKind::TypeNamed { path, .. } => path
3036                .segments
3037                .iter()
3038                .map(|s| s.name.as_str())
3039                .collect::<Vec<_>>()
3040                .join("::"),
3041            NodeKind::Identifier { name } => name.name.clone(),
3042            _ => "Unknown".into(),
3043        }
3044    }
3045
3046    /// Render one generic-param bound to its Rust trait spelling, mapping a
3047    /// compiler-provided sealed-core trait (`Equatable`/`Comparable`/`Displayable`/
3048    /// `Hashable`) that has no user `impl` to its std-trait equivalent
3049    /// (`PartialEq`/`Ord`/`std::fmt::Display`/`Hash`) — GAP-C. A `T: Equatable`
3050    /// bound references a trait that does not exist in Rust, so a primitive (or
3051    /// any) instantiation fails `E0405`; the std equivalent lets the native
3052    /// `==`/`.cmp(..)`/`.to_string()` lowering type-check. A user-declared trait of
3053    /// the same name (a real `impl` exists) keeps its name.
3054    fn rs_bound_to_string(&self, b: &bock_ast::TypePath) -> String {
3055        let name = b
3056            .segments
3057            .iter()
3058            .map(|s| s.name.as_str())
3059            .collect::<Vec<_>>()
3060            .join("::");
3061        if crate::generator::is_unimplemented_sealed_core_trait(&name, &self.trait_decls) {
3062            match name.as_str() {
3063                "Equatable" => "PartialEq".to_string(),
3064                "Comparable" => "Ord".to_string(),
3065                "Displayable" => "std::fmt::Display".to_string(),
3066                "Hashable" => "std::hash::Hash".to_string(),
3067                _ => name,
3068            }
3069        } else {
3070            name
3071        }
3072    }
3073
3074    fn generic_params_to_rs(&self, params: &[bock_ast::GenericParam]) -> String {
3075        if params.is_empty() {
3076            return String::new();
3077        }
3078        let items: Vec<String> = params
3079            .iter()
3080            .map(|p| {
3081                if p.bounds.is_empty() {
3082                    p.name.name.clone()
3083                } else {
3084                    let bounds: Vec<String> = p
3085                        .bounds
3086                        .iter()
3087                        .map(|b| self.rs_bound_to_string(b))
3088                        .collect();
3089                    format!("{}: {}", p.name.name, bounds.join(" + "))
3090                }
3091            })
3092            .collect();
3093        format!("<{}>", items.join(", "))
3094    }
3095
3096    /// DQ31 (§18.5): when a record/enum carries an explicit `impl Equatable`
3097    /// (the checker's [`bock_types::checker::CUSTOM_EQ_META_KEY`] stamp), emit a
3098    /// `PartialEq` that DELEGATES to that custom `eq`. This gives the type its
3099    /// ONE equality natively inside Rust containers — `Vec<T> == Vec<T>`,
3100    /// `HashMap`/`HashSet` membership, tuple `==` — all route through the user's
3101    /// `eq`, matching the standalone `==` (which the `"impl"` lane already
3102    /// dispatches through `eq`). Without it, `Vec<T> == Vec<T>` is `E0369` (the
3103    /// type derives no `PartialEq`); a *structural* `#[derive(PartialEq)]` would
3104    /// pin the WRONG equality. `Eq` is intentionally NOT implemented: a custom
3105    /// `eq` need not be reflexive/total, and Rust containers used here
3106    /// (`Vec`/`HashMap` value comparison via `==`) need only `PartialEq`.
3107    ///
3108    /// Emitted as a fully-qualified call to the trait method
3109    /// (`Equatable::eq`) to avoid the `PartialEq::eq` / `Equatable::eq` name
3110    /// clash on the receiver.
3111    fn emit_delegating_partial_eq(
3112        &mut self,
3113        node: &AIRNode,
3114        name: &str,
3115        generic_params: &[bock_ast::GenericParam],
3116    ) {
3117        // Read the checker's `CUSTOM_EQ_META_KEY` stamp directly (bock-codegen
3118        // sits above bock-types, so the constant is importable; the getter
3119        // lives nowhere in `generator.rs` for this lane).
3120        if !matches!(
3121            node.metadata.get(bock_types::checker::CUSTOM_EQ_META_KEY),
3122            Some(bock_air::Value::Bool(true))
3123        ) {
3124            return;
3125        }
3126        // Generic params appear in both the `impl<..>` header and the type
3127        // path; bounds on the params are preserved (the delegate calls
3128        // `Equatable::eq`, which the params must satisfy at the use site).
3129        let generics = self.generic_params_to_rs(generic_params);
3130        let type_args = if generic_params.is_empty() {
3131            String::new()
3132        } else {
3133            let names: Vec<String> = generic_params.iter().map(|p| p.name.name.clone()).collect();
3134            format!("<{}>", names.join(", "))
3135        };
3136        self.buf.push('\n');
3137        self.writeln(&format!(
3138            "impl{generics} PartialEq for {name}{type_args} {{"
3139        ));
3140        self.indent += 1;
3141        self.writeln("fn eq(&self, other: &Self) -> bool {");
3142        self.indent += 1;
3143        self.writeln("Equatable::eq(self, other)");
3144        self.indent -= 1;
3145        self.writeln("}");
3146        self.indent -= 1;
3147        self.writeln("}");
3148    }
3149
3150    /// Emit where clause: `where T: Foo, U: Bar`.
3151    fn where_clause_to_rs(&self, clauses: &[bock_ast::TypeConstraint]) -> String {
3152        if clauses.is_empty() {
3153            return String::new();
3154        }
3155        let items: Vec<String> = clauses
3156            .iter()
3157            .map(|c| {
3158                let bounds: Vec<String> = c
3159                    .bounds
3160                    .iter()
3161                    .map(|b| self.rs_bound_to_string(b))
3162                    .collect();
3163                format!("{}: {}", c.param.name, bounds.join(" + "))
3164            })
3165            .collect();
3166        format!("\nwhere\n    {}", items.join(",\n    "))
3167    }
3168
3169    /// Synthesize a minimal local definition for each §18.2 prelude
3170    /// (compiler-sealed) trait that an `impl` in this module targets but that is
3171    /// neither a user-declared trait nor imported. The prelude makes these trait
3172    /// names (`Comparable`/`Equatable`/`Displayable`/`Hashable`) resolvable
3173    /// without a `use`, but Rust has no such trait unless one is emitted; the
3174    /// declaring `core.*` module is unreachable (no real `use` edge), so codegen
3175    /// emits the contract here. The synthesized signature matches what the
3176    /// backend lowers an impl method to, so the `impl` type-checks against it.
3177    /// (Q-prelude-impl-missing-import.)
3178    fn emit_synthesized_prelude_traits(&mut self, items: &[AIRNode]) {
3179        let mut emitted: std::collections::HashSet<String> = std::collections::HashSet::new();
3180        for item in items {
3181            let NodeKind::ImplBlock {
3182                trait_path: Some(tp),
3183                ..
3184            } = &item.kind
3185            else {
3186                continue;
3187            };
3188            let Some(trait_name) = tp.segments.last().map(|s| s.name.as_str()) else {
3189                continue;
3190            };
3191            // Only an *unimplemented* sealed-core trait (no user `trait` decl)
3192            // needs synthesis; a user trait of the same name is emitted normally.
3193            if !crate::generator::is_unimplemented_sealed_core_trait(trait_name, &self.trait_decls)
3194            {
3195                continue;
3196            }
3197            if !emitted.insert(trait_name.to_string()) {
3198                continue;
3199            }
3200            // The §18.2 prelude trait contracts (fixed by spec §18.3). `Ordering`
3201            // lowers to the `std::cmp::Ordering` bridge here (the user enum is
3202            // unreachable). A trait Rust does not recognise as one of these is
3203            // skipped (no contract to synthesize).
3204            // The operand is taken BY VALUE (`other: Self`) to match the impl
3205            // method the backend emits for an unimplemented sealed-core trait:
3206            // its `other: T` param is by value and the call site `a.compare(b)`
3207            // passes by value too (these methods are NOT in `self_operand_methods`,
3208            // which is seeded only from user `trait_decls`). A `&Self` trait
3209            // declaration would mismatch the impl (E0053) and the call site
3210            // (E0308).
3211            let body = match trait_name {
3212                "Comparable" => "fn compare(&self, other: Self) -> std::cmp::Ordering;",
3213                "Equatable" => "fn eq(&self, other: Self) -> bool;",
3214                "Displayable" => "fn to_string(&self) -> String;",
3215                "Hashable" => "fn hash(&self) -> u64;",
3216                _ => continue,
3217            };
3218            self.writeln(&format!("trait {trait_name} {{"));
3219            self.indent += 1;
3220            self.writeln(body);
3221            self.indent -= 1;
3222            self.writeln("}");
3223            self.buf.push('\n');
3224        }
3225    }
3226
3227    // ── Top-level dispatch ──────────────────────────────────────────────────
3228
3229    fn emit_node(&mut self, node: &AIRNode) -> Result<(), CodegenError> {
3230        match &node.kind {
3231            NodeKind::Module { items, imports, .. } => {
3232                if self.per_module {
3233                    // Per-module native-module path (the real build): each module
3234                    // is emitted to its own `.rs` file. Record whether it
3235                    // references the concurrency runtime (emitted once into
3236                    // `bock_runtime`) and, if so, import it from there rather than
3237                    // inlining the prelude (a duplicate `struct __BockChannel`
3238                    // across files is a Rust redefinition error). Then emit real
3239                    // `use crate::<m>::<x>;` for cross-module references.
3240                    if rs_module_uses_concurrency(items) {
3241                        self.concurrency_runtime_emitted = true;
3242                        self.writeln("use crate::bock_runtime::*;");
3243                    }
3244                    self.emit_cross_module_uses(imports);
3245                } else {
3246                    // Single-module self-contained emit (`generate_module`, used
3247                    // by unit tests): the module's items are emitted into one file
3248                    // and `ImportDecl`s are dropped. The concurrency runtime is
3249                    // inlined at most once (a duplicate `struct __BockChannel`
3250                    // would not compile).
3251                    if !self.concurrency_runtime_emitted && rs_module_uses_concurrency(items) {
3252                        self.buf.push_str(CONCURRENCY_RUNTIME_RS);
3253                        self.buf.push('\n');
3254                        self.concurrency_runtime_emitted = true;
3255                    }
3256                }
3257                // §18.2 prelude-trait impls without an explicit `use`: a
3258                // `impl Comparable for Foo` whose `core.compare` is unreachable
3259                // (so no `use crate::core::compare::Comparable;` is emitted)
3260                // references a trait Rust has never seen (E0405). The prelude
3261                // makes the trait name resolvable without the import, so codegen
3262                // must provide the definition itself. Synthesize a minimal local
3263                // `trait <Name> { … }` for each sealed-core trait impl'd here that
3264                // is not a user-declared trait and not imported.
3265                // (Q-prelude-impl-missing-import.)
3266                self.emit_synthesized_prelude_traits(items);
3267                // `@test` functions are NOT emitted into the runtime module
3268                // tree: they are transpiled separately into the target's test
3269                // framework (project mode, §20.6.2 — see `generate_tests`). Their
3270                // bodies use the `expect(...)` assertion DSL, which has no runtime
3271                // definition in the emitted source, so emitting them here would
3272                // produce code that does not compile.
3273                let mut first = true;
3274                for item in items.iter() {
3275                    if crate::generator::fn_is_test(item) {
3276                        continue;
3277                    }
3278                    if !first {
3279                        self.buf.push('\n');
3280                    }
3281                    first = false;
3282                    self.emit_node(item)?;
3283                }
3284                Ok(())
3285            }
3286            NodeKind::ImportDecl { .. } => {
3287                // Resolved by the real `use crate::<m>::<x>;` statements emitted
3288                // up front by `emit_cross_module_uses` from the `Module` arm
3289                // (per-module path), or dropped entirely in the single-module
3290                // self-contained path. Either way, the per-item visit here is a
3291                // no-op.
3292                Ok(())
3293            }
3294            NodeKind::FnDecl {
3295                visibility,
3296                is_async,
3297                name,
3298                generic_params,
3299                params,
3300                return_type,
3301                effect_clause,
3302                where_clause,
3303                body,
3304                ..
3305            } => self.emit_fn_decl(
3306                *visibility,
3307                *is_async,
3308                &name.name,
3309                generic_params,
3310                params,
3311                return_type.as_deref(),
3312                effect_clause,
3313                where_clause,
3314                body,
3315            ),
3316            NodeKind::RecordDecl {
3317                visibility,
3318                name,
3319                generic_params,
3320                fields,
3321                ..
3322            } => {
3323                let vis = vis_str(*visibility);
3324                let generics = self.generic_params_to_rs(generic_params);
3325                // Derive `Clone` on every generated record. Bock value types are
3326                // freely copyable, so a generated struct must be `Clone` to be
3327                // usable as the type argument of a generic fn carrying a
3328                // `T: Clone` bound — e.g. a user `record Item` passed to
3329                // `first_or[T](xs: List[T], dflt: T)`, whose `List.get(i).cloned()`
3330                // synthesizes `where T: Clone` (GAP-B). `#[derive(Clone)]` adds
3331                // the standard per-field bound (a generic `Box<T>` derives
3332                // `Clone where T: Clone`), so this never over-constrains a
3333                // concrete instantiation whose fields are all `Clone`. The
3334                // `clone_target_records`/`clone_bound_records` sets continue to
3335                // govern the *impl bound* + `self.field.clone()` rewrite, which is
3336                // independent of the struct derive.
3337                //
3338                // DQ29 (§18.5 structural Equatable): a record the checker
3339                // stamped structurally Equatable additionally derives
3340                // `PartialEq`, so native `==`/`!=` (and containment such as
3341                // `Vec<T> == Vec<T>`) compile with field-wise semantics. The
3342                // derive's conditional per-field bounds give generic records
3343                // rule 4's per-instantiation conformance for free. A record
3344                // with an explicit `impl Equatable` is NOT stamped — its `==`
3345                // routes through `eq` (see `user_eq_kind`), and deriving a
3346                // structural `PartialEq` besides it would pin the wrong
3347                // equality into container comparisons.
3348                if crate::generator::derives_structural_eq(node) {
3349                    self.writeln("#[derive(Clone, PartialEq)]");
3350                } else {
3351                    self.writeln("#[derive(Clone)]");
3352                }
3353                self.writeln(&format!("{vis}struct {}{generics} {{", name.name));
3354                self.indent += 1;
3355                for f in fields {
3356                    let ty = self.ast_type_to_rs(&f.ty);
3357                    self.writeln(&format!("pub {}: {ty},", to_snake_case(&f.name.name)));
3358                }
3359                self.indent -= 1;
3360                self.writeln("}");
3361                self.emit_delegating_partial_eq(node, &name.name, generic_params);
3362                Ok(())
3363            }
3364            NodeKind::EnumDecl {
3365                visibility,
3366                name,
3367                generic_params,
3368                variants,
3369                ..
3370            } => {
3371                let vis = vis_str(*visibility);
3372                let generics = self.generic_params_to_rs(generic_params);
3373                // Derive `Clone` on every generated enum, for the same reason as
3374                // records (GAP-B): a user enum used as the type argument of a
3375                // generic fn with a synthesized `T: Clone` bound must be `Clone`.
3376                // DQ29: structurally-Equatable enums additionally derive
3377                // `PartialEq` (tag-then-payload equality) — see the RecordDecl
3378                // arm for the full rationale.
3379                if crate::generator::derives_structural_eq(node) {
3380                    self.writeln("#[derive(Clone, PartialEq)]");
3381                } else {
3382                    self.writeln("#[derive(Clone)]");
3383                }
3384                self.writeln(&format!("{vis}enum {}{generics} {{", name.name));
3385                self.indent += 1;
3386                for variant in variants {
3387                    self.emit_enum_variant(variant)?;
3388                }
3389                self.indent -= 1;
3390                self.writeln("}");
3391                self.emit_delegating_partial_eq(node, &name.name, generic_params);
3392                Ok(())
3393            }
3394            NodeKind::ClassDecl {
3395                visibility,
3396                name,
3397                generic_params,
3398                fields,
3399                methods,
3400                ..
3401            } => {
3402                // Rust has no classes; emit as struct + impl block.
3403                let vis = vis_str(*visibility);
3404                let generics = self.generic_params_to_rs(generic_params);
3405                // Derive `Clone` for the same reason as records/enums (GAP-B): a
3406                // class value used as a `T: Clone`-bounded generic argument must
3407                // be `Clone`.
3408                self.writeln("#[derive(Clone)]");
3409                self.writeln(&format!("{vis}struct {}{generics} {{", name.name));
3410                self.indent += 1;
3411                for f in fields {
3412                    let ty = self.ast_type_to_rs(&f.ty);
3413                    self.writeln(&format!("pub {}: {ty},", to_snake_case(&f.name.name)));
3414                }
3415                self.indent -= 1;
3416                self.writeln("}");
3417                // DQ31: a class with an explicit `impl Equatable` gets a
3418                // `PartialEq` delegating to its `eq`, so a `Vec`/`HashMap`/
3419                // `HashSet` of the class compares through the custom equality.
3420                self.emit_delegating_partial_eq(node, &name.name, generic_params);
3421                self.buf.push('\n');
3422                // impl block for methods
3423                if !methods.is_empty() {
3424                    self.writeln(&format!("impl{generics} {}{generics} {{", name.name));
3425                    self.indent += 1;
3426                    for (i, method) in methods.iter().enumerate() {
3427                        if i > 0 {
3428                            self.buf.push('\n');
3429                        }
3430                        self.emit_method(method)?;
3431                    }
3432                    self.indent -= 1;
3433                    self.writeln("}");
3434                }
3435                Ok(())
3436            }
3437            NodeKind::TraitDecl {
3438                visibility,
3439                name,
3440                generic_params,
3441                methods,
3442                ..
3443            } => {
3444                let vis = vis_str(*visibility);
3445                let generics = self.generic_params_to_rs(generic_params);
3446                self.writeln(&format!("{vis}trait {}{generics} {{", name.name));
3447                self.indent += 1;
3448                for (i, method) in methods.iter().enumerate() {
3449                    if i > 0 {
3450                        self.buf.push('\n');
3451                    }
3452                    self.emit_trait_method(method)?;
3453                }
3454                self.indent -= 1;
3455                self.writeln("}");
3456                Ok(())
3457            }
3458            NodeKind::ImplBlock {
3459                generic_params,
3460                trait_path,
3461                trait_args,
3462                target,
3463                where_clause,
3464                methods,
3465                ..
3466            } => {
3467                let target_base = self.type_expr_base_name(target);
3468                let target_rendered = self.type_expr_to_string(target);
3469                // Resolve the params the impl introduces. When the impl declares
3470                // its own (`impl[T] Box[T]`), use them and trust the target the
3471                // user wrote. When it declares none but the target is a generic
3472                // record/enum (`impl Box { ... }`, `T` on `record Box[T]`), Rust
3473                // requires the impl to both introduce and apply the params:
3474                // synthesize `impl<T> Box<T>`.
3475                let synth_params: Vec<bock_ast::GenericParam> = if generic_params.is_empty() {
3476                    self.generic_decls
3477                        .get(&target_base)
3478                        .cloned()
3479                        .unwrap_or_default()
3480                } else {
3481                    generic_params.to_vec()
3482                };
3483                // A *generic* impl needs a `T: Clone` bound when the generated
3484                // body clones a generic value — either by moving a `self.field`
3485                // out by value (`return self.v` / `return Some(self.v)`, lowered
3486                // to `self.v.clone()`) or by calling a built-in collection method
3487                // the codegen lowers with `.cloned()` / `.clone()` (`List.get` /
3488                // `concat`, `Map.get`, a `Set` op). The pre-scan
3489                // `clone_target_records` already flags the bare field-return
3490                // getters; here we additionally cover trait impls and the
3491                // collection-clone case so a generic `impl P[T] for R[T]` whose
3492                // `f` does `return Some(self.v)`, or a generic iterator whose
3493                // `next` does `self.xs.get(...)`, carries the bound. Only generic
3494                // impls qualify (`!synth_params.is_empty()`) — a concrete record
3495                // moving a non-`Copy` field is the orthogonal `&self` move-out
3496                // defect, left untouched.
3497                let is_generic_impl = !synth_params.is_empty();
3498                let any_method_moves_self = methods
3499                    .iter()
3500                    .any(|m| matches!(&m.kind, NodeKind::FnDecl { body, .. } if Self::body_moves_self_field(body)));
3501                let any_method_clones_collection = methods.iter().any(|m| {
3502                    matches!(&m.kind, NodeKind::FnDecl { body, .. } if Self::body_clones_collection_element(body))
3503                });
3504                let add_clone_bound = is_generic_impl
3505                    && (self.clone_target_records.contains(&target_base)
3506                        || any_method_moves_self
3507                        || any_method_clones_collection);
3508                let generics = self.generic_params_to_rs_with_clone(&synth_params, add_clone_bound);
3509                // The applied target type. Prefer the form the user wrote if it
3510                // already carries args (`impl Box[T]`); otherwise synthesize
3511                // `Box<T>` from the recovered params.
3512                let target_name = if !generic_params.is_empty() || synth_params.is_empty() {
3513                    target_rendered
3514                } else {
3515                    format!("{target_base}{}", self.generic_param_args_rs(&synth_params))
3516                };
3517                let where_cl = self.where_clause_to_rs(where_clause);
3518                if let Some(tp) = trait_path {
3519                    let mut trait_name = tp
3520                        .segments
3521                        .iter()
3522                        .map(|s| s.name.as_str())
3523                        .collect::<Vec<_>>()
3524                        .join("::");
3525                    // Trait type arguments: `impl From<Int> for Float`.
3526                    if !trait_args.is_empty() {
3527                        let args: Vec<String> =
3528                            trait_args.iter().map(|a| self.type_to_rs(a)).collect();
3529                        trait_name.push_str(&format!("<{}>", args.join(", ")));
3530                    }
3531                    self.writeln(&format!(
3532                        "impl{generics} {trait_name} for {target_name}{where_cl} {{"
3533                    ));
3534                } else {
3535                    self.writeln(&format!("impl{generics} {target_name}{where_cl} {{"));
3536                }
3537                let suppress_vis = trait_path.is_some();
3538                let prev_clone_self = self.in_clone_self_method;
3539                self.indent += 1;
3540                for (i, method) in methods.iter().enumerate() {
3541                    if i > 0 {
3542                        self.buf.push('\n');
3543                    }
3544                    // `in_clone_self_method` controls whether a `self.field` read
3545                    // emits `.clone()`. Set it *per method* and only for methods
3546                    // that genuinely move a `self.field` out by value — never for
3547                    // a method that merely reads/assigns a field (cloning the LHS
3548                    // of `self.cursor = ...` would emit invalid Rust).
3549                    //
3550                    // A `&self` Rust method cannot move a non-`Copy` field out,
3551                    // so the `self.field` read is lowered to `self.field.clone()`
3552                    // whether the receiver is generic or concrete — e.g.
3553                    // `impl Iterable[Int] for Bag { fn iter(self) {
3554                    // list_iter(self.items) } }` moves the concrete `Vec<i64>`
3555                    // field out of `&self` (`E0507`). For a *generic* receiver
3556                    // the matching `T: Clone` bound must be in scope; the
3557                    // impl-level `add_clone_bound` predicate already guarantees
3558                    // `is_generic_impl && method_moves_self ⟹ add_clone_bound`,
3559                    // so dropping the conjunct here only newly enables the clone
3560                    // for concrete receivers (whose field type is itself
3561                    // clonable, no bound required).
3562                    let method_moves_self = matches!(
3563                        &method.kind,
3564                        NodeKind::FnDecl { body, .. } if Self::body_moves_self_field(body)
3565                    );
3566                    self.in_clone_self_method = method_moves_self;
3567                    self.emit_method_inner(method, suppress_vis)?;
3568                }
3569                self.indent -= 1;
3570                self.in_clone_self_method = prev_clone_self;
3571                self.writeln("}");
3572                // Q-displayable-interpolation-dispatch: a user `impl Displayable`
3573                // (its `to_string` is the type's display form) ALSO gets a
3574                // delegating `impl std::fmt::Display`, so a `${p}` interpolation
3575                // — lowered to `format!("…{}…", p)` — renders through the user's
3576                // `to_string` rather than failing `E0277` (`Point` doesn't
3577                // implement `Display`). Mirrors the DQ31 delegating-`PartialEq`
3578                // pattern. Fires only for a `Displayable` impl that actually
3579                // declares a `to_string` instance method.
3580                if let Some(tp) = trait_path {
3581                    let trait_leaf = tp.segments.last().map(|s| s.name.as_str());
3582                    let has_to_string = methods.iter().any(|m| {
3583                        matches!(&m.kind, NodeKind::FnDecl { name, .. } if name.name == "to_string")
3584                    });
3585                    if trait_leaf == Some("Displayable") && has_to_string {
3586                        self.buf.push('\n');
3587                        self.writeln(&format!(
3588                            "impl{generics} std::fmt::Display for {target_name}{where_cl} {{"
3589                        ));
3590                        self.indent += 1;
3591                        self.writeln(
3592                            "fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {",
3593                        );
3594                        self.indent += 1;
3595                        // Fully-qualify through the trait so it is unambiguous
3596                        // whether or not the type also has an inherent `to_string`
3597                        // (and so it works against the §18.2-synthesized
3598                        // `trait Displayable`).
3599                        self.writeln("write!(f, \"{}\", Displayable::to_string(self))");
3600                        self.indent -= 1;
3601                        self.writeln("}");
3602                        self.indent -= 1;
3603                        self.writeln("}");
3604                    }
3605                }
3606                Ok(())
3607            }
3608            NodeKind::EffectDecl {
3609                visibility,
3610                name,
3611                components,
3612                generic_params,
3613                operations,
3614                ..
3615            } => {
3616                if !components.is_empty() {
3617                    let comp_names: Vec<String> = components
3618                        .iter()
3619                        .map(|tp| {
3620                            tp.segments
3621                                .last()
3622                                .map_or("effect".to_string(), |s| s.name.clone())
3623                        })
3624                        .collect();
3625                    self.writeln(&format!(
3626                        "// composite effect {} = {}",
3627                        name.name,
3628                        comp_names.join(" + ")
3629                    ));
3630                    self.composite_effects.insert(name.name.clone(), comp_names);
3631                    return Ok(());
3632                }
3633                // Record effect operations for Call → handler.op rewriting.
3634                for op in operations {
3635                    if let NodeKind::FnDecl { name: op_name, .. } = &op.kind {
3636                        self.effect_ops
3637                            .insert(op_name.name.clone(), name.name.clone());
3638                    }
3639                }
3640                // Effects → Rust traits with `&dyn` usage.
3641                let vis = vis_str(*visibility);
3642                let generics = self.generic_params_to_rs(generic_params);
3643                self.writeln(&format!("{vis}trait {}{generics} {{", name.name));
3644                self.indent += 1;
3645                for (i, op) in operations.iter().enumerate() {
3646                    if i > 0 {
3647                        self.buf.push('\n');
3648                    }
3649                    self.emit_trait_method(op)?;
3650                }
3651                self.indent -= 1;
3652                self.writeln("}");
3653                Ok(())
3654            }
3655            NodeKind::TypeAlias {
3656                visibility,
3657                name,
3658                generic_params,
3659                ty,
3660                where_clause,
3661                ..
3662            } => {
3663                let vis = vis_str(*visibility);
3664                let generics = self.generic_params_to_rs(generic_params);
3665                let ty_str = self.type_to_rs(ty);
3666                let where_cl = self.where_clause_to_rs(where_clause);
3667                self.writeln(&format!(
3668                    "{vis}type {}{generics}{where_cl} = {ty_str};",
3669                    name.name
3670                ));
3671                Ok(())
3672            }
3673            NodeKind::ConstDecl {
3674                visibility,
3675                name,
3676                value,
3677                ty,
3678                ..
3679            } => {
3680                let vis = vis_str(*visibility);
3681                let ty_str = self.type_to_rs(ty);
3682                let ind = self.indent_str();
3683                let _ = write!(
3684                    self.buf,
3685                    "{ind}{vis}const {}: {ty_str} = ",
3686                    to_upper_snake_case(&name.name)
3687                );
3688                self.emit_expr(value)?;
3689                self.buf.push_str(";\n");
3690                Ok(())
3691            }
3692            NodeKind::ModuleHandle { effect, handler } => {
3693                // Module-level `handle` becomes a `const` whose type is the
3694                // concrete handler struct. Referring to `&CONST` in call
3695                // positions produces a valid `&impl Trait` borrow without
3696                // the `Sync`/`'static` requirements that `static &dyn Trait`
3697                // would impose. The handler is registered as the default
3698                // for this effect, so subsequent effectful calls pass it
3699                // implicitly unless a local handling block overrides it.
3700                let effect_name = effect.segments.last().map_or("effect", |s| s.name.as_str());
3701                let const_name = format!("__{}_HANDLER", to_snake_case(effect_name).to_uppercase());
3702                let handler_type = record_construct_type(handler);
3703                let ind = self.indent_str();
3704                if let Some(type_name) = handler_type {
3705                    let _ = write!(self.buf, "{ind}const {const_name}: {type_name} = ");
3706                    self.emit_expr(handler)?;
3707                    self.buf.push_str(";\n");
3708                    self.current_handler_vars
3709                        .insert(effect_name.to_string(), const_name);
3710                    // A module-level `handle` const is a concrete owned handler;
3711                    // forwarding it borrows (`&CONST`), so it is not a borrowed
3712                    // param.
3713                    self.borrowed_handler_effects.remove(effect_name);
3714                } else {
3715                    // Fallback for non-literal handlers: emit a comment so the
3716                    // output is still valid Rust but the handler must be
3717                    // provided at every call site.
3718                    let _ = write!(self.buf, "{ind}// module handle: {effect_name} with ");
3719                    self.emit_expr(handler)?;
3720                    self.buf.push('\n');
3721                }
3722                Ok(())
3723            }
3724            NodeKind::PropertyTest { name, .. } => {
3725                self.writeln(&format!("// property test: {name}"));
3726                Ok(())
3727            }
3728            // Statement / expression nodes at top level:
3729            NodeKind::LetBinding { .. }
3730            | NodeKind::If { .. }
3731            | NodeKind::For { .. }
3732            | NodeKind::While { .. }
3733            | NodeKind::Loop { .. }
3734            | NodeKind::Return { .. }
3735            | NodeKind::Break { .. }
3736            | NodeKind::Continue
3737            | NodeKind::Guard { .. }
3738            | NodeKind::Match { .. }
3739            | NodeKind::Block { .. }
3740            | NodeKind::HandlingBlock { .. }
3741            | NodeKind::Assign { .. } => self.emit_stmt(node),
3742            // Expression nodes that appear as statements:
3743            _ => {
3744                self.write_indent();
3745                self.emit_expr(node)?;
3746                self.buf.push_str(";\n");
3747                Ok(())
3748            }
3749        }
3750    }
3751
3752    // ── Function declarations ───────────────────────────────────────────────
3753
3754    #[allow(clippy::too_many_arguments)]
3755    fn emit_fn_decl(
3756        &mut self,
3757        visibility: Visibility,
3758        is_async: bool,
3759        name: &str,
3760        generic_params: &[bock_ast::GenericParam],
3761        params: &[AIRNode],
3762        return_type: Option<&AIRNode>,
3763        effect_clause: &[bock_ast::TypePath],
3764        where_clause: &[bock_ast::TypeConstraint],
3765        body: &AIRNode,
3766    ) -> Result<(), CodegenError> {
3767        let vis = vis_str(visibility);
3768        let async_kw = if is_async { "async " } else { "" };
3769        // A generic free function whose body clones a generic element via a
3770        // built-in collection method (`List.get`/`concat`, `Map.get`, a `Set`
3771        // op — each lowered with `.cloned()` / `.clone()`) needs a `T: Clone`
3772        // bound, just like the generic-impl case. Without it `dup[T](xs:
3773        // List[T])` returning `xs.concat(xs)` fails with `E0277: T: Clone is not
3774        // satisfied`. Only generic functions qualify, and only when such a clone
3775        // is actually emitted.
3776        //
3777        // It also needs the bound *transitively*: a fn that takes a clone-bound
3778        // record by value (`ListIterator[T]`, whose `impl` requires `T: Clone`)
3779        // and drives it with a method call must propagate that bound, or
3780        // method resolution fails (`count[T]`/`fold[T,A]` calling `it.next()` →
3781        // `E0599`). See `params_drive_clone_bound_record`.
3782        let add_clone_bound = !generic_params.is_empty()
3783            && (Self::body_clones_collection_element(body)
3784                || Self::body_reuses_match_binding(body)
3785                || self.params_drive_clone_bound_record(params, body));
3786        let generics = self.generic_params_to_rs_with_clone(generic_params, add_clone_bound);
3787        // A function whose declared return type is a `Fn(..) -> ..` returns an
3788        // `impl Fn` (a closure). Its closure params then need `+ 'static` and the
3789        // tail closure must `move`-capture — see `returning_fn_closure`.
3790        let returns_fn_closure = return_type.is_some_and(|t| self.type_is_fn_closure(t));
3791        let param_strs = if returns_fn_closure {
3792            self.collect_param_strs_static_fn(params)
3793        } else {
3794            self.collect_param_strs(params)
3795        };
3796        let effects = self.effects_params(effect_clause);
3797        let mut all_params = param_strs;
3798        all_params.extend(effects);
3799        let ret = return_type
3800            .map(|t| format!(" -> {}", self.type_to_rs_fn_pos(t)))
3801            .unwrap_or_default();
3802        let where_cl = self.where_clause_to_rs(where_clause);
3803        if !effect_clause.is_empty() {
3804            let effect_names = self.expand_effect_names(effect_clause);
3805            self.fn_effects.insert(name.to_string(), effect_names);
3806        }
3807        let fn_name = to_snake_case(name);
3808        // `async fn main` needs a runtime attribute — tokio drives the future
3809        // to completion on a multi-threaded executor, matching the Bock
3810        // interpreter's async runtime model.
3811        if is_async && fn_name == "main" {
3812            self.writeln("#[tokio::main]");
3813        }
3814        self.writeln(&format!(
3815            "{vis}{async_kw}fn {fn_name}{generics}({}){ret}{where_cl} {{",
3816            all_params.join(", "),
3817        ));
3818        self.indent += 1;
3819        let old_handler_vars = self.current_handler_vars.clone();
3820        let old_borrowed_handlers = self.borrowed_handler_effects.clone();
3821        let expanded = self.expand_effect_names(effect_clause);
3822        for ename in &expanded {
3823            self.current_handler_vars
3824                .insert(ename.clone(), to_snake_case(ename));
3825            // This fn's effect param is `&impl Effect` — already a reference, so
3826            // a nested effectful call forwards it as-is (not re-borrowed).
3827            self.borrowed_handler_effects.insert(ename.clone());
3828        }
3829        // A by-value, non-`Copy` parameter reused after a move must clone on each
3830        // by-value pass (`E0382`). See `seed_reused_params`. The function's
3831        // generic-param names keep a generic `T` value out of the bare-tail clone
3832        // set (it may lack `Clone` — E0599).
3833        let generic_names = Self::generic_param_name_set(generic_params);
3834        let seeded = self.seed_reused_params(params, body, &generic_names);
3835        let prev_returning = self.return_closure_tail;
3836        // The flag is consulted only at the function's tail expression (the
3837        // returned value), so an intermediate `.map`/`.filter` closure in the
3838        // body is unaffected — only the returned closure gets `move`. See
3839        // `returning_fn_closure` / `return_closure_tail`.
3840        self.return_closure_tail = returns_fn_closure;
3841        self.emit_block_body(body)?;
3842        self.return_closure_tail = prev_returning;
3843        for name in seeded {
3844            self.reused_let_bindings.remove(&name);
3845            self.reused_value_tail_bindings.remove(&name);
3846        }
3847        self.current_handler_vars = old_handler_vars;
3848        self.borrowed_handler_effects = old_borrowed_handlers;
3849        self.indent -= 1;
3850        self.writeln("}");
3851        Ok(())
3852    }
3853
3854    /// Emit a method inside an impl block (with `&self` / `&mut self`).
3855    /// If `suppress_vis` is true, visibility qualifiers are omitted (e.g. trait impl methods).
3856    fn emit_method_inner(
3857        &mut self,
3858        method: &AIRNode,
3859        suppress_vis: bool,
3860    ) -> Result<(), CodegenError> {
3861        if let NodeKind::FnDecl {
3862            visibility,
3863            is_async,
3864            name,
3865            generic_params,
3866            params,
3867            return_type,
3868            effect_clause,
3869            where_clause,
3870            body,
3871            ..
3872        } = &method.kind
3873        {
3874            let vis = if suppress_vis {
3875                ""
3876            } else {
3877                vis_str(*visibility)
3878            };
3879            let async_kw = if *is_async { "async " } else { "" };
3880            let generics = self.generic_params_to_rs(generic_params);
3881            // The AIR keeps `self` as a leading `Param`; consume it to form the
3882            // native Rust receiver and emit the remaining params positionally.
3883            // Without this the method gets both `&self` and a `self: _` param.
3884            //
3885            // An *associated function* (no `self` receiver, e.g. a `From` impl's
3886            // `from(value) -> Self`) is a native Rust associated fn — emit no
3887            // receiver at all. Synthesizing `&self` here would not match the
3888            // trait signature (`fn from(T) -> Self`) and Rust rejects it (E0185).
3889            let is_assoc = crate::generator::is_associated_impl_method(method, &self.effect_ops);
3890            let (receiver, rest) = match params.first().map(crate::generator::param_binds_self) {
3891                Some(Some(is_mut)) => {
3892                    let recv = if is_mut { "&mut self" } else { "&self" };
3893                    (Some(recv.to_string()), &params[1..])
3894                }
3895                _ if is_assoc => (None, &params[..]),
3896                _ => (Some("&self".to_string()), &params[..]),
3897            };
3898            // A `Self`-operand trait method's impl borrows its operand to match
3899            // the trait signature (`fn compare(&self, other: &Key)`); the call
3900            // site borrows the argument to match. See `self_operand_methods`.
3901            let borrow_operands = self.self_operand_methods.contains(&name.name);
3902            let param_strs = self.collect_param_strs_inner(rest, borrow_operands, false);
3903            let effects = self.effects_params(effect_clause);
3904            let mut all_params: Vec<String> = receiver.into_iter().collect();
3905            all_params.extend(param_strs);
3906            all_params.extend(effects);
3907            let ret = return_type
3908                .as_deref()
3909                .map(|t| format!(" -> {}", self.type_to_rs(t)))
3910                .unwrap_or_default();
3911            let where_cl = self.where_clause_to_rs(where_clause);
3912            let fn_name = to_snake_case(&name.name);
3913            self.writeln(&format!(
3914                "{vis}{async_kw}fn {fn_name}{generics}({}){ret}{where_cl} {{",
3915                all_params.join(", "),
3916            ));
3917            self.indent += 1;
3918            let old_handler_vars = self.current_handler_vars.clone();
3919            let old_borrowed_handlers = self.borrowed_handler_effects.clone();
3920            let expanded = self.expand_effect_names(effect_clause);
3921            for ename in &expanded {
3922                self.current_handler_vars
3923                    .insert(ename.clone(), to_snake_case(ename));
3924                // `&impl Effect` method param — forward as-is, never re-borrowed.
3925                self.borrowed_handler_effects.insert(ename.clone());
3926            }
3927            // Seed move-reuse clones for by-value, non-`Copy` method params (the
3928            // `self` receiver is borrowed and skipped). See `seed_reused_params`.
3929            let generic_names = Self::generic_param_name_set(generic_params);
3930            let seeded = self.seed_reused_params(rest, body, &generic_names);
3931            self.emit_block_body(body)?;
3932            for name in seeded {
3933                self.reused_let_bindings.remove(&name);
3934                self.reused_value_tail_bindings.remove(&name);
3935            }
3936            self.current_handler_vars = old_handler_vars;
3937            self.borrowed_handler_effects = old_borrowed_handlers;
3938            self.indent -= 1;
3939            self.writeln("}");
3940        }
3941        Ok(())
3942    }
3943
3944    /// Emit a method with visibility preserved.
3945    fn emit_method(&mut self, method: &AIRNode) -> Result<(), CodegenError> {
3946        self.emit_method_inner(method, false)
3947    }
3948
3949    /// Emit a trait method signature (may or may not have a body).
3950    fn emit_trait_method(&mut self, method: &AIRNode) -> Result<(), CodegenError> {
3951        if let NodeKind::FnDecl {
3952            is_async,
3953            name,
3954            generic_params,
3955            params,
3956            return_type,
3957            effect_clause,
3958            where_clause,
3959            body,
3960            ..
3961        } = &method.kind
3962        {
3963            let async_kw = if *is_async { "async " } else { "" };
3964            let generics = self.generic_params_to_rs(generic_params);
3965            // Q-prim-assoc: an *associated function* trait method (no `self`
3966            // receiver — `From::from(value: T) -> Self`, `TryFrom::try_from`)
3967            // must be declared WITHOUT a receiver. The previous `_ =>` arm
3968            // injected a spurious `&self` on every receiver-less method, which
3969            // made `core.convert`'s `From`/`TryFrom` traits uncompilable on Rust
3970            // (`E0186`: impl has no `&self` to match). Effect operations also
3971            // lack a `self` param but ARE instance methods on a handler, so
3972            // `is_associated_impl_method` excludes them and keeps their `&self`.
3973            let is_assoc = crate::generator::is_associated_impl_method(method, &self.effect_ops);
3974            let (receiver, rest): (Option<String>, &[AIRNode]) =
3975                match params.first().map(crate::generator::param_binds_self) {
3976                    Some(Some(is_mut)) => {
3977                        let recv = if is_mut { "&mut self" } else { "&self" };
3978                        (Some(recv.to_string()), &params[1..])
3979                    }
3980                    _ if is_assoc => (None, &params[..]),
3981                    _ => (Some("&self".to_string()), &params[..]),
3982                };
3983            // A `Self`-operand trait method (`compare`/`eq`/…) takes its operand
3984            // by shared reference, so the bound value can be reused after the
3985            // call (Bock value semantics) — see `self_operand_methods`.
3986            let borrow_operands = self.self_operand_methods.contains(&name.name);
3987            let param_strs = self.collect_param_strs_inner(rest, borrow_operands, false);
3988            let effects = self.effects_params(effect_clause);
3989            let mut all_params: Vec<String> = receiver.into_iter().collect();
3990            all_params.extend(param_strs);
3991            all_params.extend(effects);
3992            let ret = return_type
3993                .as_deref()
3994                .map(|t| format!(" -> {}", self.type_to_rs(t)))
3995                .unwrap_or_default();
3996            let mut where_cl = self.where_clause_to_rs(where_clause);
3997            let fn_name = to_snake_case(&name.name);
3998
3999            // A default method (one carrying a body) that still takes a `Self`
4000            // operand *by value* needs `where Self: Sized` (inside the trait
4001            // `Self` is `?Sized`). A borrowed operand (`other: &Self`) is always
4002            // sized, so the bound is unnecessary there.
4003            let has_body = crate::generator::is_default_method(method);
4004            // Q-prim-assoc: an associated function (no `self` receiver) that
4005            // *returns* a `Self`-bearing type by value — `From::from -> Self`,
4006            // `TryFrom::try_from -> Result<Self, ConvertError>` — likewise needs
4007            // `where Self: Sized` (inside a trait `Self` is `?Sized`, so a
4008            // by-value `Self` return is otherwise `E0277`-rejected).
4009            let assoc_returns_self = is_assoc && ret.contains("Self");
4010            let needs_sized =
4011                (has_body && !borrow_operands && rest.iter().any(Self::param_type_is_self))
4012                    || assoc_returns_self;
4013            if needs_sized {
4014                if where_cl.is_empty() {
4015                    where_cl = " where Self: Sized".to_string();
4016                } else {
4017                    where_cl = format!("{where_cl},\n    Self: Sized");
4018                }
4019            }
4020
4021            if has_body {
4022                self.writeln(&format!(
4023                    "{async_kw}fn {fn_name}{generics}({}){ret}{where_cl} {{",
4024                    all_params.join(", "),
4025                ));
4026                self.indent += 1;
4027                self.emit_block_body(body)?;
4028                self.indent -= 1;
4029                self.writeln("}");
4030            } else {
4031                self.writeln(&format!(
4032                    "{async_kw}fn {fn_name}{generics}({}){ret}{where_cl};",
4033                    all_params.join(", "),
4034                ));
4035            }
4036        }
4037        Ok(())
4038    }
4039
4040    /// True if `param` is a `Param` node whose declared type is exactly `Self`.
4041    /// Used to decide whether a default trait method needs `where Self: Sized`
4042    /// (a by-value `Self` operand is `?Sized` inside the trait).
4043    fn param_type_is_self(param: &AIRNode) -> bool {
4044        matches!(
4045            &param.kind,
4046            NodeKind::Param { ty: Some(t), .. } if matches!(t.kind, NodeKind::TypeSelf)
4047        )
4048    }
4049
4050    fn collect_param_strs(&mut self, params: &[AIRNode]) -> Vec<String> {
4051        self.collect_param_strs_inner(params, false, false)
4052    }
4053
4054    /// As [`Self::collect_param_strs`], but every `Fn`-typed param gains a
4055    /// `+ 'static` bound on its `impl Fn` lowering. Used for the params of a
4056    /// function that returns a closure — see [`Self::returning_fn_closure`].
4057    fn collect_param_strs_static_fn(&mut self, params: &[AIRNode]) -> Vec<String> {
4058        self.collect_param_strs_inner(params, false, true)
4059    }
4060
4061    /// As [`Self::collect_param_strs`], but when `borrow` is set each param's
4062    /// declared type is emitted as a shared reference (`other: &Target`). Used
4063    /// for the operands of a `Self`-operand trait method (`compare`/`eq`/…),
4064    /// which Rust must take by reference to match Bock's value semantics. When
4065    /// `static_fn` is set, a `Fn`-typed param's `impl Fn` lowering gains
4066    /// `+ 'static` (a closure-returning function — see `collect_param_strs_static_fn`).
4067    fn collect_param_strs_inner(
4068        &mut self,
4069        params: &[AIRNode],
4070        borrow: bool,
4071        static_fn: bool,
4072    ) -> Vec<String> {
4073        let mut result = Vec::new();
4074        for p in params {
4075            if let NodeKind::Param {
4076                pattern,
4077                ty,
4078                default,
4079            } = &p.kind
4080            {
4081                let name = to_snake_case(&self.pattern_to_binding_name(pattern));
4082                // A `mut`-bound param (`fn f(mut items: …)`) may reassign its
4083                // binding in the body (`items = …`). Rust requires the binding
4084                // be declared `mut` or the reassignment is E0384. The borrow
4085                // form (`&Target`) takes the operand by shared reference and is
4086                // never reassigned, so it never gets `mut`.
4087                let mut_kw =
4088                    if !borrow && matches!(&pattern.kind, NodeKind::BindPat { is_mut: true, .. }) {
4089                        "mut "
4090                    } else {
4091                        ""
4092                    };
4093                let amp = if borrow { "&" } else { "" };
4094                let type_ann = ty
4095                    .as_ref()
4096                    .map(|t| format!(": {amp}{}", self.type_to_rs_fn_pos_bounded(t, static_fn)))
4097                    .unwrap_or_else(|| ": _".into());
4098                if let Some(def) = default {
4099                    // Rust doesn't have default params; emit a comment.
4100                    let mut ctx = RsEmitCtx::new();
4101                    ctx.indent = self.indent;
4102                    if ctx.emit_expr(def).is_ok() {
4103                        let def_str = ctx.buf;
4104                        result.push(format!("{mut_kw}{name}{type_ann} /* = {def_str} */"));
4105                        continue;
4106                    }
4107                }
4108                result.push(format!("{mut_kw}{name}{type_ann}"));
4109            }
4110        }
4111        result
4112    }
4113
4114    /// Expand effect names, replacing composite effects with their components.
4115    fn expand_effect_names(&self, effects: &[bock_ast::TypePath]) -> Vec<String> {
4116        let mut result = Vec::new();
4117        for tp in effects {
4118            let name = tp
4119                .segments
4120                .last()
4121                .map_or("effect".to_string(), |s| s.name.clone());
4122            if let Some(components) = self.composite_effects.get(&name) {
4123                result.extend(components.iter().cloned());
4124            } else {
4125                result.push(name);
4126            }
4127        }
4128        result
4129    }
4130
4131    /// The in-scope `Clock` effect handler variable, if one is installed.
4132    ///
4133    /// When `Some`, the `Clock` time operations (`Instant.now`, `sleep`,
4134    /// `elapsed`) are routed through the handler instead of inlining the host
4135    /// primitive (Q-clock-handler-routing, §18.3.1/§18.4); when `None`, no
4136    /// handler is in scope and the default host primitive is emitted.
4137    fn clock_handler_var(&self) -> Option<&str> {
4138        self.current_handler_vars.get("Clock").map(String::as_str)
4139    }
4140
4141    /// Effects → `&impl EffectTrait` parameters (argument-position impl trait).
4142    /// This gives each effectful function a fresh generic parameter per effect,
4143    /// so handlers can be any concrete type implementing the effect trait while
4144    /// keeping the ownership story simple: the caller retains ownership and the
4145    /// function borrows for its body.
4146    fn effects_params(&self, effects: &[bock_ast::TypePath]) -> Vec<String> {
4147        let expanded = self.expand_effect_names(effects);
4148        expanded
4149            .iter()
4150            .map(|name| {
4151                let param_name = to_snake_case(name);
4152                format!("{param_name}: &impl {name}")
4153            })
4154            .collect()
4155    }
4156
4157    /// Build the handler arguments for calling an effectful function. Each effect
4158    /// of the callee is forwarded from the current scope's handler variable: a
4159    /// concrete owned handler (module-level `handle` const, a `handling`-block
4160    /// local) is borrowed (`&handler`); an *already-borrowed* `&impl Effect`
4161    /// parameter is forwarded as-is (`handler`), since re-borrowing it would be
4162    /// `&&impl Effect` and fail the trait bound (`E0277`). See
4163    /// [`Self::borrowed_handler_effects`].
4164    fn build_effects_call_args_rs(&self, fn_name: &str) -> Option<String> {
4165        let effects = self.fn_effects.get(fn_name)?;
4166        let entries: Vec<String> = effects
4167            .iter()
4168            .filter_map(|e| {
4169                let handler_var = self.current_handler_vars.get(e)?;
4170                if self.borrowed_handler_effects.contains(e) {
4171                    Some(handler_var.clone())
4172                } else {
4173                    Some(format!("&{handler_var}"))
4174                }
4175            })
4176            .collect();
4177        if entries.is_empty() {
4178            return None;
4179        }
4180        Some(entries.join(", "))
4181    }
4182
4183    // ── Enum variant ────────────────────────────────────────────────────────
4184
4185    fn emit_enum_variant(&mut self, variant: &AIRNode) -> Result<(), CodegenError> {
4186        if let NodeKind::EnumVariant { name, payload } = &variant.kind {
4187            let vname = &name.name;
4188            match payload {
4189                EnumVariantPayload::Unit => {
4190                    self.writeln(&format!("{vname},"));
4191                }
4192                EnumVariantPayload::Struct(fields) => {
4193                    self.writeln(&format!("{vname} {{"));
4194                    self.indent += 1;
4195                    for f in fields {
4196                        let ty = self.ast_type_to_rs(&f.ty);
4197                        self.writeln(&format!("{}: {ty},", to_snake_case(&f.name.name)));
4198                    }
4199                    self.indent -= 1;
4200                    self.writeln("},");
4201                }
4202                EnumVariantPayload::Tuple(elems) => {
4203                    let types: Vec<String> = elems.iter().map(|e| self.type_to_rs(e)).collect();
4204                    self.writeln(&format!("{vname}({}),", types.join(", ")));
4205                }
4206            }
4207        }
4208        Ok(())
4209    }
4210
4211    // ── Statements ──────────────────────────────────────────────────────────
4212
4213    fn emit_stmt(&mut self, node: &AIRNode) -> Result<(), CodegenError> {
4214        match &node.kind {
4215            NodeKind::LetBinding {
4216                pattern,
4217                value,
4218                ty,
4219                is_mut,
4220            } => {
4221                // Declare-only temp from the shared value-CF hoist: Rust allows a
4222                // deferred-init `let mut x;` when every path assigns before use,
4223                // which the relocated control flow guarantees. The type is
4224                // inferred from the assignment(s).
4225                if node.metadata.contains_key(crate::generator::DECL_ONLY_META) {
4226                    let binding = self.pattern_to_rs_binding(pattern);
4227                    let ind = self.indent_str();
4228                    let type_ann = ty
4229                        .as_ref()
4230                        .map(|t| format!(": {}", self.type_to_rs(t)))
4231                        .unwrap_or_default();
4232                    let _ = writeln!(self.buf, "{ind}let mut {binding}{type_ann};");
4233                    return Ok(());
4234                }
4235                let binding = self.pattern_to_rs_binding(pattern);
4236                let type_ann = ty
4237                    .as_ref()
4238                    .map(|t| format!(": {}", self.type_to_rs(t)))
4239                    .unwrap_or_default();
4240                let mut_kw = if *is_mut { "mut " } else { "" };
4241                let ind = self.indent_str();
4242                let _ = write!(self.buf, "{ind}let {mut_kw}{binding}{type_ann} = ");
4243                let wrap_task = matches!(&value.kind, NodeKind::Call { .. })
4244                    && self.task_bound_names.contains(&binding);
4245                if wrap_task {
4246                    self.buf.push_str("tokio::spawn(");
4247                    self.emit_expr(value)?;
4248                    self.buf.push(')');
4249                } else {
4250                    self.emit_expr(value)?;
4251                }
4252                self.buf.push_str(";\n");
4253                Ok(())
4254            }
4255            NodeKind::If {
4256                let_pattern,
4257                condition,
4258                then_block,
4259                else_block,
4260            } => {
4261                let ind = self.indent_str();
4262                if let Some(pat) = let_pattern {
4263                    let binding = self.pattern_to_rs_binding(pat);
4264                    let _ = write!(self.buf, "{ind}if let Some({binding}) = ");
4265                    self.emit_expr(condition)?;
4266                    self.buf.push_str(" {\n");
4267                } else {
4268                    let _ = write!(self.buf, "{ind}if ");
4269                    self.emit_expr(condition)?;
4270                    self.buf.push_str(" {\n");
4271                }
4272                self.indent += 1;
4273                self.emit_block_body(then_block)?;
4274                self.indent -= 1;
4275                if let Some(else_b) = else_block {
4276                    if matches!(else_b.kind, NodeKind::If { .. }) {
4277                        let ind = self.indent_str();
4278                        let _ = write!(self.buf, "{ind}}} else ");
4279                        // Remove leading indent from recursive call
4280                        self.emit_if_continuing(else_b)?;
4281                        return Ok(());
4282                    }
4283                    self.writeln("} else {");
4284                    self.indent += 1;
4285                    self.emit_block_body(else_b)?;
4286                    self.indent -= 1;
4287                }
4288                self.writeln("}");
4289                Ok(())
4290            }
4291            NodeKind::For {
4292                pattern,
4293                iterable,
4294                body,
4295            } => {
4296                let binding = self.pattern_to_rs_binding(pattern);
4297                let ind = self.indent_str();
4298                let _ = write!(self.buf, "{ind}for {binding} in ");
4299                // `for x in coll` consumes `coll` via `.into_iter()`. If `coll` is
4300                // a binding reused after the loop (`render_document(nodes)` after
4301                // `for n in nodes`), clone it so the later use stays live
4302                // (`E0382`). The loop body keeps owned element bindings (matching
4303                // Bock's by-value `for`); iterating `&coll` would change them to
4304                // references and break the body.
4305                //
4306                // The iterable may be a *field access* of a reused binding
4307                // (`for row in dataset.rows` while `dataset` is used again after
4308                // the loop): iterating `dataset.rows` partially moves `dataset`,
4309                // so a later `dataset.clone()`/`dataset.field` is a use of a
4310                // partially-moved value (`E0382`). Cloning the field access
4311                // (`dataset.rows.clone()`) leaves the owner intact.
4312                let clone_iter = self.iterable_is_reused(iterable);
4313                self.emit_expr(iterable)?;
4314                if clone_iter {
4315                    self.buf.push_str(".clone()");
4316                }
4317                self.buf.push_str(" {\n");
4318                self.indent += 1;
4319                // A loop body's tail is not a function return — disarm the
4320                // closure-return `move` so a loop-tail closure isn't `move`d.
4321                let prev_tail = std::mem::replace(&mut self.return_closure_tail, false);
4322                // The loop variable binds an owned, by-value element each
4323                // iteration (Bock's by-value `for`). If the body passes it
4324                // by value to a call and *also* reads it afterward
4325                // (`is_category(e, …)` then `e.amount`), the first pass moves it
4326                // (`E0382`). Seed it as a reused binding so the call-arg emitter
4327                // clones the by-value pass. Move-reused when read by value more
4328                // than once, OR read even once inside a NESTED loop — a nested
4329                // `for` re-executes that by-value read on every inner iteration,
4330                // so the loop var is moved on the first inner pass and gone on
4331                // the second (`for tag in tags { for n in nums { label(tag) } }`,
4332                // the nested-loop shape). This mirrors `seed_reused_params`'
4333                // `count > 1 || identifier_used_in_loop` heuristic. A `Copy`
4334                // scalar is never moved (cloning it is needless noise).
4335                let prev_reused = self.reused_let_bindings.clone();
4336                let mut loop_bindings = Vec::new();
4337                Self::collect_pattern_binding_names(pattern, &mut loop_bindings);
4338                for b in &loop_bindings {
4339                    let rs_name = to_snake_case(b);
4340                    if Self::count_identifier_uses(body, &rs_name) > 1
4341                        || Self::identifier_used_in_loop(body, &rs_name)
4342                    {
4343                        self.reused_let_bindings.insert(rs_name);
4344                    }
4345                }
4346                self.emit_block_body(body)?;
4347                self.reused_let_bindings = prev_reused;
4348                self.return_closure_tail = prev_tail;
4349                self.indent -= 1;
4350                self.writeln("}");
4351                Ok(())
4352            }
4353            NodeKind::While { condition, body } => {
4354                let ind = self.indent_str();
4355                let _ = write!(self.buf, "{ind}while ");
4356                self.emit_expr(condition)?;
4357                self.buf.push_str(" {\n");
4358                self.indent += 1;
4359                let prev_tail = std::mem::replace(&mut self.return_closure_tail, false);
4360                self.emit_block_body(body)?;
4361                self.return_closure_tail = prev_tail;
4362                self.indent -= 1;
4363                self.writeln("}");
4364                Ok(())
4365            }
4366            NodeKind::Loop { body } => {
4367                self.writeln("loop {");
4368                self.indent += 1;
4369                let prev_tail = std::mem::replace(&mut self.return_closure_tail, false);
4370                self.emit_block_body(body)?;
4371                self.return_closure_tail = prev_tail;
4372                self.indent -= 1;
4373                self.writeln("}");
4374                Ok(())
4375            }
4376            NodeKind::Return { value } => {
4377                if let Some(val) = value {
4378                    let ind = self.indent_str();
4379                    let _ = write!(self.buf, "{ind}return ");
4380                    self.emit_expr(val)?;
4381                    self.buf.push_str(";\n");
4382                } else {
4383                    self.writeln("return;");
4384                }
4385                Ok(())
4386            }
4387            NodeKind::Break { value } => {
4388                if let Some(val) = value {
4389                    let ind = self.indent_str();
4390                    let _ = write!(self.buf, "{ind}break ");
4391                    self.emit_expr(val)?;
4392                    self.buf.push_str(";\n");
4393                } else {
4394                    self.writeln("break;");
4395                }
4396                Ok(())
4397            }
4398            NodeKind::Continue => {
4399                self.writeln("continue;");
4400                Ok(())
4401            }
4402            NodeKind::Guard {
4403                let_pattern,
4404                condition,
4405                else_block,
4406            } => {
4407                let ind = self.indent_str();
4408                if let Some(pat) = let_pattern {
4409                    // `guard (let PAT = EXPR) else { … }` lowers to Rust's
4410                    // `let-else`: `let PAT = EXPR else { … };`. The pattern's
4411                    // bindings stay in scope for the rest of the enclosing
4412                    // block (the whole point of guard-let), and the else arm
4413                    // must diverge — which Bock's guard semantics already
4414                    // guarantee. Lowering it to a boolean `if !(cond)` instead
4415                    // drops the bindings (E0425) and negates a non-bool value
4416                    // (E0600); `let-else` is the faithful form.
4417                    let _ = write!(self.buf, "{ind}let ");
4418                    self.emit_pattern(pat)?;
4419                    self.buf.push_str(" = ");
4420                    self.emit_expr(condition)?;
4421                    self.buf.push_str(" else {\n");
4422                    self.indent += 1;
4423                    self.emit_block_body(else_block)?;
4424                    self.indent -= 1;
4425                    self.writeln("};");
4426                } else {
4427                    let _ = write!(self.buf, "{ind}if !(");
4428                    self.emit_expr(condition)?;
4429                    self.buf.push_str(") {\n");
4430                    self.indent += 1;
4431                    self.emit_block_body(else_block)?;
4432                    self.indent -= 1;
4433                    self.writeln("}");
4434                }
4435                Ok(())
4436            }
4437            NodeKind::Match { scrutinee, arms } => self.emit_match(scrutinee, arms),
4438            NodeKind::Block { stmts, tail } => {
4439                for s in stmts {
4440                    self.emit_node(s)?;
4441                }
4442                if let Some(t) = tail {
4443                    self.write_indent();
4444                    self.emit_expr(t)?;
4445                    self.buf.push('\n');
4446                }
4447                Ok(())
4448            }
4449            NodeKind::HandlingBlock { handlers, body } => {
4450                // handling block → scoped handler instantiation
4451                self.writeln("{");
4452                self.indent += 1;
4453                let old_handler_vars = self.current_handler_vars.clone();
4454                let old_borrowed_handlers = self.borrowed_handler_effects.clone();
4455                for h in handlers {
4456                    let effect_name = h
4457                        .effect
4458                        .segments
4459                        .last()
4460                        .map_or("effect", |s| s.name.as_str());
4461                    let var_name = format!("__{}", to_snake_case(effect_name));
4462                    let ind = self.indent_str();
4463                    let _ = write!(self.buf, "{ind}let {var_name} = ");
4464                    self.emit_expr(&h.handler)?;
4465                    self.buf.push_str(";\n");
4466                    self.current_handler_vars
4467                        .insert(effect_name.to_string(), var_name);
4468                    // A `handling`-block local is a concrete owned handler value,
4469                    // so forwarding it borrows (`&__effect`) — clear any inherited
4470                    // borrowed-param marker for this effect.
4471                    self.borrowed_handler_effects.remove(effect_name);
4472                }
4473                if let NodeKind::Block { stmts, tail } = &body.kind {
4474                    for s in stmts {
4475                        self.emit_node(s)?;
4476                    }
4477                    if let Some(t) = tail {
4478                        self.write_indent();
4479                        self.emit_expr(t)?;
4480                        self.buf.push('\n');
4481                    }
4482                } else {
4483                    self.emit_stmt(body)?;
4484                }
4485                self.current_handler_vars = old_handler_vars;
4486                self.borrowed_handler_effects = old_borrowed_handlers;
4487                self.indent -= 1;
4488                self.writeln("}");
4489                Ok(())
4490            }
4491            NodeKind::Assign { op, target, value } => {
4492                let ind = self.indent_str();
4493                let _ = write!(self.buf, "{ind}");
4494                // The target is a place expression; suppress the clone-self
4495                // `self.field` → `self.field.clone()` rewrite while emitting it.
4496                let prev_assign_target = self.in_assign_target;
4497                self.in_assign_target = true;
4498                let target_res = self.emit_expr(target);
4499                self.in_assign_target = prev_assign_target;
4500                target_res?;
4501                let op_str = match op {
4502                    AssignOp::Assign => " = ",
4503                    AssignOp::AddAssign => " += ",
4504                    AssignOp::SubAssign => " -= ",
4505                    AssignOp::MulAssign => " *= ",
4506                    AssignOp::DivAssign => " /= ",
4507                    AssignOp::RemAssign => " %= ",
4508                };
4509                self.buf.push_str(op_str);
4510                self.emit_expr(value)?;
4511                self.buf.push_str(";\n");
4512                Ok(())
4513            }
4514            _ => {
4515                self.write_indent();
4516                self.emit_expr(node)?;
4517                self.buf.push_str(";\n");
4518                Ok(())
4519            }
4520        }
4521    }
4522
4523    /// Helper for chained if/else if without extra indent.
4524    fn emit_if_continuing(&mut self, node: &AIRNode) -> Result<(), CodegenError> {
4525        if let NodeKind::If {
4526            let_pattern,
4527            condition,
4528            then_block,
4529            else_block,
4530        } = &node.kind
4531        {
4532            if let Some(pat) = let_pattern {
4533                let binding = self.pattern_to_rs_binding(pat);
4534                let _ = write!(self.buf, "if let Some({binding}) = ");
4535                self.emit_expr(condition)?;
4536                self.buf.push_str(" {\n");
4537            } else {
4538                let _ = write!(self.buf, "if ");
4539                self.emit_expr(condition)?;
4540                self.buf.push_str(" {\n");
4541            }
4542            self.indent += 1;
4543            self.emit_block_body(then_block)?;
4544            self.indent -= 1;
4545            if let Some(else_b) = else_block {
4546                if matches!(else_b.kind, NodeKind::If { .. }) {
4547                    let ind = self.indent_str();
4548                    let _ = write!(self.buf, "{ind}}} else ");
4549                    self.emit_if_continuing(else_b)?;
4550                    return Ok(());
4551                }
4552                self.writeln("} else {");
4553                self.indent += 1;
4554                self.emit_block_body(else_b)?;
4555                self.indent -= 1;
4556            }
4557            self.writeln("}");
4558        }
4559        Ok(())
4560    }
4561
4562    // ── Expressions ─────────────────────────────────────────────────────────
4563
4564    fn emit_expr(&mut self, node: &AIRNode) -> Result<(), CodegenError> {
4565        match &node.kind {
4566            NodeKind::Literal { lit } => {
4567                match lit {
4568                    Literal::Int(s) => {
4569                        self.buf.push_str(s);
4570                        self.buf.push_str("_i64");
4571                    }
4572                    Literal::Float(s) => {
4573                        self.buf.push_str(s);
4574                        self.buf.push_str("_f64");
4575                    }
4576                    Literal::Bool(b) => {
4577                        self.buf.push_str(if *b { "true" } else { "false" });
4578                    }
4579                    Literal::Char(s) => {
4580                        self.buf.push('\'');
4581                        self.buf.push_str(s);
4582                        self.buf.push('\'');
4583                    }
4584                    Literal::String(s) => {
4585                        self.buf.push('"');
4586                        self.buf.push_str(&escape_rs_string(s));
4587                        self.buf.push('"');
4588                        self.buf.push_str(".to_string()");
4589                    }
4590                    Literal::Unit => self.buf.push_str("()"),
4591                }
4592                Ok(())
4593            }
4594            NodeKind::Identifier { name } => {
4595                // The prelude `Ordering` variants map to Rust's native
4596                // `std::cmp::Ordering` — UNLESS the real `core.compare.Ordering`
4597                // enum is reachable, in which case the references must use that
4598                // user enum (handled by the `variant_enum_qualifier_for_name`
4599                // path below). This mirrors how `Some`/`None` map to
4600                // `std::option`.
4601                if crate::generator::ordering_variant(&name.name).is_some()
4602                    && !self.ordering_enum_reachable()
4603                {
4604                    let variant = &name.name;
4605                    let _ = write!(self.buf, "std::cmp::Ordering::{variant}");
4606                    return Ok(());
4607                }
4608                // A bare identifier naming a registered unit variant is a
4609                // construction (`Empty` → `Shape::Empty`); Rust requires the
4610                // enum qualifier.
4611                if let Some(enum_name) = self.variant_enum_qualifier_for_name(&name.name) {
4612                    let _ = write!(self.buf, "{enum_name}::{}", name.name);
4613                } else {
4614                    self.buf.push_str(&identifier_to_rs(&name.name));
4615                }
4616                Ok(())
4617            }
4618            NodeKind::BinaryOp { op, left, right } => {
4619                // `+` on two `List[T]` operands is concatenation. Rust does not
4620                // implement `Add` for `Vec<T>` (E0369), so clone the left operand
4621                // and extend it with the right — value-semantic concat that leaves
4622                // both operands usable. `T: Clone` holds for every v1 element type.
4623                if *op == BinOp::Add && crate::generator::is_list_concat(node, left, right) {
4624                    let l = self.expr_to_string(left)?;
4625                    let r = self.expr_to_string(right)?;
4626                    let _ = write!(
4627                        self.buf,
4628                        "{{ let mut __v = ({l}).clone(); __v.extend(({r}).iter().cloned()); __v }}"
4629                    );
4630                    return Ok(());
4631                }
4632                // `String + String` concat: Rust's `String + String` does not
4633                // compile (`Add<String>` is not implemented; only `String +
4634                // &str`). Emit `format!("{}{}", l, r)`, which concatenates
4635                // regardless of whether each side is an owned `String` or `&str`.
4636                // The checker's `string_concat` stamp is authoritative (it sees
4637                // operand *types*, so it catches `result + sep` where both are
4638                // `String`-typed identifiers); the syntactic `expr_is_string_rs`
4639                // heuristic is the fallback for unstamped nodes.
4640                let string_concat_stamped = matches!(
4641                    node.metadata
4642                        .get(bock_types::checker::STRING_CONCAT_META_KEY),
4643                    Some(bock_air::Value::Bool(true))
4644                );
4645                if *op == BinOp::Add
4646                    && (string_concat_stamped
4647                        || Self::expr_is_string_rs(left)
4648                        || Self::expr_is_string_rs(right))
4649                {
4650                    let l = self.expr_to_string(left)?;
4651                    let r = self.expr_to_string(right)?;
4652                    let _ = write!(self.buf, "format!(\"{{}}{{}}\", {l}, {r})");
4653                    return Ok(());
4654                }
4655                // `**` has no Rust operator. An `Int ** Int` lowers to
4656                // `i64::pow`, whose exponent is `u32` (not `i64`) — so the
4657                // exponent is cast `(rhs) as u32` (E0308 otherwise). A
4658                // `Float ** _` lowers to `f64::powf`, whose exponent is `f64`;
4659                // a Float-literal operand on either side selects this path so
4660                // `b ** 3.0` type-checks instead of calling the (nonexistent)
4661                // `f64::pow`. The float exponent is cast `(rhs) as f64` to
4662                // admit an integer-literal exponent (`2.0 ** 3`).
4663                if *op == BinOp::Pow {
4664                    if Self::pow_is_float(left, right) {
4665                        self.buf.push('(');
4666                        self.emit_expr(left)?;
4667                        self.buf.push_str(").powf((");
4668                        self.emit_expr(right)?;
4669                        self.buf.push_str(") as f64)");
4670                    } else {
4671                        self.buf.push('(');
4672                        self.emit_expr(left)?;
4673                        self.buf.push_str(").pow((");
4674                        self.emit_expr(right)?;
4675                        self.buf.push_str(") as u32)");
4676                    }
4677                    return Ok(());
4678                }
4679                // Ordering operators on a user `Comparable` type lower through the
4680                // type's `compare` (Rust structs are not `PartialOrd`, so native
4681                // `<` is E0369). Emit `matches!(a.compare(&b), Ordering::Less)` —
4682                // `matches!` recognises the returned variant without requiring the
4683                // `Ordering` enum to derive `PartialEq` (an `==` would be E0369),
4684                // mirroring how the enum's `match` arms already work. The operand
4685                // is borrowed exactly as a hand-written `a.compare(b)` would
4686                // (`compare` is a `Self`-operand method).
4687                if crate::generator::is_user_compare(node) {
4688                    if let Some((tag, is_eq)) = crate::generator::user_compare_variant(*op) {
4689                        let neg = if is_eq { "" } else { "!" };
4690                        let _ = write!(self.buf, "{neg}matches!(");
4691                        self.emit_expr(left)?;
4692                        self.buf.push_str(".compare(");
4693                        // `compare` takes its operand by shared reference; borrow it
4694                        // the same way the desugared self-call path does.
4695                        self.emit_call_arg(right, true)?;
4696                        let _ = write!(self.buf, "), Ordering::{tag})");
4697                        return Ok(());
4698                    }
4699                }
4700                // DQ29: `==`/`!=` on a type with an explicit `impl Equatable`
4701                // dispatch through its `eq` (the user's `eq` IS the type's
4702                // equality). DQ31 (§18.5): such a type ALSO gets a `PartialEq`
4703                // that delegates to that same `eq` (see
4704                // `emit_delegating_partial_eq`), so `self.eq(other)` is now
4705                // ambiguous between `Equatable::eq` and `PartialEq::eq` — use
4706                // the fully-qualified trait call `Equatable::eq(self, other)`.
4707                // The "structural"/"deep" lanes stay native: the stamped
4708                // structural derive gives records/enums field-wise `==`, and
4709                // `Vec`/`HashMap`/`HashSet`/tuples are already structural in
4710                // Rust. The "deep_custom" lane (a container whose element tree
4711                // carries a custom impl) ALSO stays native: the delegating
4712                // `PartialEq` makes `Vec<T> == Vec<T>` / `HashMap` / `HashSet`
4713                // route element comparison, key-matching and membership through
4714                // the element's `eq`.
4715                if matches!(op, BinOp::Eq | BinOp::Ne)
4716                    && crate::generator::user_eq_kind(node) == Some("impl")
4717                {
4718                    if *op == BinOp::Ne {
4719                        self.buf.push('!');
4720                    }
4721                    self.buf.push_str("Equatable::eq(&");
4722                    self.emit_expr(left)?;
4723                    self.buf.push_str(", &");
4724                    self.emit_expr(right)?;
4725                    self.buf.push(')');
4726                    return Ok(());
4727                }
4728                self.buf.push('(');
4729                self.emit_expr(left)?;
4730                let op_str = match op {
4731                    BinOp::Add => " + ",
4732                    BinOp::Sub => " - ",
4733                    BinOp::Mul => " * ",
4734                    BinOp::Div => " / ",
4735                    BinOp::Rem => " % ",
4736                    BinOp::Pow => unreachable!("Pow handled above"),
4737                    BinOp::Eq => " == ",
4738                    BinOp::Ne => " != ",
4739                    BinOp::Lt => " < ",
4740                    BinOp::Le => " <= ",
4741                    BinOp::Gt => " > ",
4742                    BinOp::Ge => " >= ",
4743                    BinOp::And => " && ",
4744                    BinOp::Or => " || ",
4745                    BinOp::BitAnd => " & ",
4746                    BinOp::BitOr => " | ",
4747                    BinOp::BitXor => " ^ ",
4748                    BinOp::Compose => " /* compose */ ",
4749                    BinOp::Is => " /* is */ ",
4750                };
4751                self.buf.push_str(op_str);
4752                self.emit_expr(right)?;
4753                self.buf.push(')');
4754                Ok(())
4755            }
4756            NodeKind::UnaryOp { op, operand } => {
4757                let op_str = match op {
4758                    UnaryOp::Neg => "-",
4759                    UnaryOp::Not => "!",
4760                    UnaryOp::BitNot => "!",
4761                };
4762                self.buf.push_str(op_str);
4763                self.emit_expr(operand)?;
4764                Ok(())
4765            }
4766            NodeKind::Call { callee, args, .. } => {
4767                // Rewrite bare effect operation calls: log(...) → handler.log(...)
4768                if let NodeKind::Identifier { name } = &callee.kind {
4769                    if let Some(effect_name) = self.effect_ops.get(&name.name).cloned() {
4770                        if let Some(handler_var) =
4771                            self.current_handler_vars.get(&effect_name).cloned()
4772                        {
4773                            let _ =
4774                                write!(self.buf, "{}.{}", handler_var, to_snake_case(&name.name));
4775                            self.buf.push('(');
4776                            for (i, arg) in args.iter().enumerate() {
4777                                if i > 0 {
4778                                    self.buf.push_str(", ");
4779                                }
4780                                // A by-value pass of a reused binding into an
4781                                // effect op (`storage.write(key, value)` before a
4782                                // later `format!("…", key)`) moves it; clone (or
4783                                // borrow a reused closure) so the later use stays
4784                                // live (`E0382`/`E0599`). See `emit_call_arg`.
4785                                self.emit_call_arg(&arg.value, false)?;
4786                            }
4787                            self.buf.push(')');
4788                            return Ok(());
4789                        }
4790                    }
4791                }
4792                if let Some(code) = self.map_prelude_call(callee, args)? {
4793                    self.buf.push_str(&code);
4794                    return Ok(());
4795                }
4796                // A call whose callee names a registered tuple variant is a
4797                // construction (`Rect(3.0, 4.0)` → `Shape::Rect(3.0, 4.0)`).
4798                if let NodeKind::Identifier { name } = &callee.kind {
4799                    if let Some(enum_name) = self.variant_enum_qualifier_for_name(&name.name) {
4800                        let _ = write!(self.buf, "{enum_name}::{}(", name.name);
4801                        for (i, arg) in args.iter().enumerate() {
4802                            if i > 0 {
4803                                self.buf.push_str(", ");
4804                            }
4805                            self.emit_expr(&arg.value)?;
4806                        }
4807                        self.buf.push(')');
4808                        return Ok(());
4809                    }
4810                }
4811                if self.try_emit_time_assoc_call(callee, args)? {
4812                    return Ok(());
4813                }
4814                if self.try_emit_time_desugared_method(callee, args)? {
4815                    return Ok(());
4816                }
4817                if self.try_emit_concurrency_call(callee, args)? {
4818                    return Ok(());
4819                }
4820                // Map/Set dispatch precedes the List recogniser so the
4821                // overlapping method names route by `recv_kind`, not by name.
4822                if self.try_emit_map_method(node, callee, args)? {
4823                    return Ok(());
4824                }
4825                if self.try_emit_set_method(node, callee, args)? {
4826                    return Ok(());
4827                }
4828                // String method dispatch runs *before* the List recogniser so the
4829                // overlapping `len`/`contains`/`is_empty` names route by the
4830                // checker's `recv_kind = "Primitive:String"`, not by name alone.
4831                if self.try_emit_string_method(node, callee, args)? {
4832                    return Ok(());
4833                }
4834                // Numeric/Char/Bool primitive methods (`to_float`/`abs`/`sqrt`/…)
4835                // likewise route by the checker's `recv_kind = "Primitive:Int|…"`
4836                // before the generic fall-through, which would emit `n.to_float(n)`
4837                // (no such inherent method on `i64`/`f64`).
4838                if self.try_emit_numeric_method(node, callee, args)? {
4839                    return Ok(());
4840                }
4841                if self.try_emit_list_mutating_method(node, callee, args)? {
4842                    return Ok(());
4843                }
4844                if self.try_emit_list_inplace_mutator(node, callee, args)? {
4845                    return Ok(());
4846                }
4847                if self.try_emit_list_method(node, callee, args)? {
4848                    return Ok(());
4849                }
4850                if self.try_emit_list_functional_method(node, callee, args)? {
4851                    return Ok(());
4852                }
4853                if self.try_emit_primitive_bridge(node, callee, args)? {
4854                    return Ok(());
4855                }
4856                if self.try_emit_trait_bound_bridge(node, callee, args)? {
4857                    return Ok(());
4858                }
4859                if self.try_emit_container_method(node, callee, args)? {
4860                    return Ok(());
4861                }
4862                // Q-prim-assoc: a primitive associated-conversion call
4863                // (`Float.from(x)` / `Int.try_from(s)` / `String.from(c)`)
4864                // lowers to Rust's native conversion, NOT `Float::from(...)`
4865                // (`f64::from(i64)` does not exist — it is lossy and unimpl'd; a
4866                // primitive type-name path like `Float::from` is also wrong).
4867                if self.try_emit_primitive_conversion(node, callee, args)? {
4868                    return Ok(());
4869                }
4870                // Associated-function call (`Type.method(args)` — stamped by the
4871                // lowerer, no `self` prepended) is a native Rust associated fn:
4872                // emit `Type::method(args)` with the type name preserved (the
4873                // `::` path syntax, not the value-receiver `.` form the generic
4874                // fall-through would produce by snake-casing the type name).
4875                if crate::generator::is_associated_call(node) {
4876                    if let NodeKind::FieldAccess { object, field } = &callee.kind {
4877                        if let NodeKind::Identifier { name: type_name } = &object.kind {
4878                            let _ = write!(
4879                                self.buf,
4880                                "{}::{}",
4881                                type_name.name,
4882                                to_snake_case(&field.name)
4883                            );
4884                            self.buf.push('(');
4885                            for (i, arg) in args.iter().enumerate() {
4886                                if i > 0 {
4887                                    self.buf.push_str(", ");
4888                                }
4889                                self.emit_call_arg(&arg.value, false)?;
4890                            }
4891                            self.buf.push(')');
4892                            return Ok(());
4893                        }
4894                    }
4895                }
4896                // Desugared instance method call `Call(FieldAccess(recv, m),
4897                // [recv, ...rest])`: emit `recv.m(rest)` so the receiver flows
4898                // through Rust's native `&self`, not as a duplicated argument.
4899                if let Some((recv, method, rest)) =
4900                    crate::generator::desugared_self_call(callee, args)
4901                {
4902                    // Q-rust-equatable-eq-collision: a user `impl Equatable`'s
4903                    // `eq` on a type that also carries the DQ31 delegating
4904                    // `PartialEq` (both have an `eq(&self, &Self) -> bool`) makes
4905                    // a value-receiver `a.eq(&b)` ambiguous (E0034). Emit the
4906                    // fully-qualified trait call `Equatable::eq(&a, &b)` instead.
4907                    // Gated on the checker's `recv_kind = "User:<T>"` stamp where
4908                    // `<T>` is one of the explicit-`impl Equatable` types — so a
4909                    // same-named inherent `eq` on a non-Equatable type (no
4910                    // delegating `PartialEq`, no ambiguity) keeps the plain
4911                    // method form.
4912                    if method.name == "eq" {
4913                        if let Some(ty) = crate::generator::raw_recv_kind(node)
4914                            .and_then(|k| k.strip_prefix("User:"))
4915                        {
4916                            if self.user_equatable_types.contains(ty) {
4917                                if let Some(other) = rest.first() {
4918                                    self.buf.push_str("Equatable::eq(&");
4919                                    self.emit_expr(recv)?;
4920                                    self.buf.push_str(", &");
4921                                    self.emit_expr(&other.value)?;
4922                                    self.buf.push(')');
4923                                    return Ok(());
4924                                }
4925                            }
4926                        }
4927                    }
4928                    self.emit_expr(recv)?;
4929                    let _ = write!(self.buf, ".{}", to_snake_case(&method.name));
4930                    self.buf.push('(');
4931                    // A `Self`-operand trait method takes its operand by shared
4932                    // reference (`a.compare(&b)`) so the caller can keep using
4933                    // the value afterwards. See `self_operand_methods`.
4934                    let borrow_operands = self.self_operand_methods.contains(&method.name);
4935                    for (i, arg) in rest.iter().enumerate() {
4936                        if i > 0 {
4937                            self.buf.push_str(", ");
4938                        }
4939                        // A `Self`-operand is borrowed; otherwise clone a reused
4940                        // binding / reused-owner field, or borrow a reused closure
4941                        // binding (`E0382`/`E0599`). See `emit_call_arg`.
4942                        self.emit_call_arg(&arg.value, borrow_operands)?;
4943                    }
4944                    self.buf.push(')');
4945                    return Ok(());
4946                }
4947                // Pass handler args to effectful function calls.
4948                let effects_args = if let NodeKind::Identifier { name } = &callee.kind {
4949                    self.build_effects_call_args_rs(&name.name)
4950                } else {
4951                    None
4952                };
4953                self.emit_callee_rs(callee)?;
4954                self.buf.push('(');
4955                // A `recv.m(b)` whose callee is a `Self`-operand trait method but
4956                // which is NOT the desugared self-call shape (the receiver isn't
4957                // duplicated into the arg list, so it falls here) still borrows
4958                // its operand: `recv.m(&b)`. The leading receiver, if present,
4959                // is a method receiver (consumed by `recv.m`), so all positional
4960                // args here are operands.
4961                let borrow_operands = matches!(
4962                    &callee.kind,
4963                    NodeKind::FieldAccess { field, .. }
4964                        if self.self_operand_methods.contains(&field.name)
4965                );
4966                for (i, arg) in args.iter().enumerate() {
4967                    if i > 0 {
4968                        self.buf.push_str(", ");
4969                    }
4970                    // A `Self`-operand is borrowed; otherwise clone a reused
4971                    // match/let binding or a reused-owner field (`filter`'s
4972                    // `pred(x)` before a later `[x]`), or borrow a reused closure
4973                    // binding (`E0382`/`E0599`). See `emit_call_arg`.
4974                    self.emit_call_arg(&arg.value, borrow_operands)?;
4975                }
4976                if let Some(ea) = effects_args {
4977                    if !args.is_empty() {
4978                        self.buf.push_str(", ");
4979                    }
4980                    self.buf.push_str(&ea);
4981                }
4982                self.buf.push(')');
4983                Ok(())
4984            }
4985            NodeKind::MethodCall {
4986                receiver,
4987                method,
4988                args,
4989                ..
4990            } => {
4991                if self.try_emit_time_method(receiver, &method.name, args)? {
4992                    return Ok(());
4993                }
4994                self.emit_expr(receiver)?;
4995                let _ = write!(self.buf, ".{}", to_snake_case(&method.name));
4996                self.buf.push('(');
4997                let borrow_operands = self.self_operand_methods.contains(&method.name);
4998                for (i, arg) in args.iter().enumerate() {
4999                    if i > 0 {
5000                        self.buf.push_str(", ");
5001                    }
5002                    // A `Self`-operand is borrowed; otherwise clone a reused
5003                    // binding / reused-owner field, or borrow a reused closure
5004                    // binding (`E0382`/`E0599`). See `emit_call_arg`.
5005                    self.emit_call_arg(&arg.value, borrow_operands)?;
5006                }
5007                self.buf.push(')');
5008                Ok(())
5009            }
5010            NodeKind::FieldAccess { object, field } => {
5011                self.emit_expr(object)?;
5012                let _ = write!(self.buf, ".{}", to_snake_case(&field.name));
5013                // Inside a clone-self method, reading a `self` field yields it by
5014                // value; a `&self` receiver cannot move a non-`Copy` field out, so
5015                // clone it. The impl carries the matching `T: Clone` bound (when
5016                // generic) and the record derives `Clone`. Never on an assignment
5017                // target (`self.cursor = …`): that is a place expression, and
5018                // `self.cursor.clone() = …` is invalid Rust.
5019                if self.in_clone_self_method
5020                    && !self.in_assign_target
5021                    && matches!(&object.kind, NodeKind::Identifier { name } if name.name == "self")
5022                {
5023                    self.buf.push_str(".clone()");
5024                }
5025                Ok(())
5026            }
5027            NodeKind::Index { object, index } => {
5028                self.emit_expr(object)?;
5029                self.buf.push('[');
5030                self.emit_expr(index)?;
5031                self.buf.push(']');
5032                Ok(())
5033            }
5034            NodeKind::Lambda { params, body } => {
5035                let param_strs = self.collect_param_strs(params);
5036                // A closure returned in tail position of a closure-returning
5037                // function must `move`-capture (the returned `impl Fn` outlives
5038                // the frame). See `returning_fn_closure`.
5039                let move_kw = if self.returning_fn_closure {
5040                    "move "
5041                } else {
5042                    ""
5043                };
5044                // A closure *nested* inside the returned one is not itself the
5045                // return value — disarm so it doesn't also get `move`.
5046                let prev_ret = std::mem::replace(&mut self.returning_fn_closure, false);
5047                let prev_tail = std::mem::replace(&mut self.return_closure_tail, false);
5048                let _ = write!(self.buf, "{move_kw}|{}| ", param_strs.join(", "));
5049                // A closure used as `.map`/`.filter`/… is `FnMut`/`Fn` — it may
5050                // run many times, so a by-value pass of a *captured* (non-param)
5051                // binding moves it out of the closure on the first call (`E0507`).
5052                // Seed every captured binding the body references for the
5053                // move-reuse clone path so the call-arg emitter clones it
5054                // (`category_name(cat)` → `category_name(cat.clone())`). The
5055                // closure's own params bind fresh values each call and are
5056                // excluded. Cloning is always sound (all generated types are
5057                // `Clone`); a captured `Copy` scalar clones harmlessly.
5058                let mut lambda_params = Vec::new();
5059                for p in params {
5060                    if let NodeKind::Param { pattern, .. } = &p.kind {
5061                        Self::collect_pattern_binding_names(pattern, &mut lambda_params);
5062                    }
5063                }
5064                let lambda_params: std::collections::HashSet<String> = lambda_params
5065                    .into_iter()
5066                    .map(|n| to_snake_case(&n))
5067                    .collect();
5068                let mut captured = Vec::new();
5069                Self::collect_identifier_names(body, &mut captured);
5070                let prev_reused_let = self.reused_let_bindings.clone();
5071                for name in captured {
5072                    if !lambda_params.contains(&name) {
5073                        self.reused_let_bindings.insert(name);
5074                    }
5075                }
5076                let r = self.emit_expr(body);
5077                self.reused_let_bindings = prev_reused_let;
5078                self.returning_fn_closure = prev_ret;
5079                self.return_closure_tail = prev_tail;
5080                r?;
5081                Ok(())
5082            }
5083            NodeKind::Pipe { left, right } => self.emit_pipe(left, right),
5084            NodeKind::Compose { left, right } => {
5085                // `f >> g` → `|x| g(f(x))`. In tail position of a closure-
5086                // returning function the composed closure captures `f`/`g`, so
5087                // it must `move` (the returned `impl Fn` outlives the frame).
5088                let move_kw = if self.returning_fn_closure {
5089                    "move "
5090                } else {
5091                    ""
5092                };
5093                let prev_ret = std::mem::replace(&mut self.returning_fn_closure, false);
5094                let prev_tail = std::mem::replace(&mut self.return_closure_tail, false);
5095                let _ = write!(self.buf, "{move_kw}|x| ");
5096                self.emit_expr(right)?;
5097                self.buf.push('(');
5098                self.emit_expr(left)?;
5099                self.buf.push_str("(x))");
5100                self.returning_fn_closure = prev_ret;
5101                self.return_closure_tail = prev_tail;
5102                Ok(())
5103            }
5104            NodeKind::Await { expr } => {
5105                // `await x` where `x` was spawned above becomes
5106                // `x.await.unwrap()` — `tokio::spawn` returns a `JoinHandle<T>`
5107                // whose `.await` yields `Result<T, JoinError>`, so we unwrap
5108                // to restore the original `T` type the user wrote.
5109                let is_spawned_handle = if let NodeKind::Identifier { name } = &expr.kind {
5110                    self.task_bound_names.contains(&to_snake_case(&name.name))
5111                } else {
5112                    false
5113                };
5114                self.emit_expr(expr)?;
5115                if is_spawned_handle {
5116                    self.buf.push_str(".await.unwrap()");
5117                } else {
5118                    self.buf.push_str(".await");
5119                }
5120                Ok(())
5121            }
5122            NodeKind::Propagate { expr } => {
5123                self.emit_expr(expr)?;
5124                self.buf.push('?');
5125                Ok(())
5126            }
5127            NodeKind::Range { lo, hi, inclusive } => {
5128                self.emit_expr(lo)?;
5129                if *inclusive {
5130                    self.buf.push_str("..=");
5131                } else {
5132                    self.buf.push_str("..");
5133                }
5134                self.emit_expr(hi)?;
5135                Ok(())
5136            }
5137            NodeKind::RecordConstruct {
5138                path,
5139                fields,
5140                spread,
5141            } => {
5142                // A struct-variant construction (`Circle { radius: .. }`) must
5143                // be qualified `Shape::Circle { .. }`; a plain record keeps its
5144                // path unqualified.
5145                let type_name = if let Some(enum_name) = self.variant_enum_qualifier(path) {
5146                    let variant = path.segments.last().map_or("", |s| s.name.as_str());
5147                    format!("{enum_name}::{variant}")
5148                } else {
5149                    path.segments
5150                        .iter()
5151                        .map(|s| s.name.as_str())
5152                        .collect::<Vec<_>>()
5153                        .join("::")
5154                };
5155                self.buf.push_str(&type_name);
5156                self.buf.push_str(" { ");
5157                for (i, f) in fields.iter().enumerate() {
5158                    if i > 0 {
5159                        self.buf.push_str(", ");
5160                    }
5161                    let fname = to_snake_case(&f.name.name);
5162                    if let Some(val) = &f.value {
5163                        let _ = write!(self.buf, "{fname}: ");
5164                        // A record-literal field value is a by-value position,
5165                        // exactly like a call argument: a reused, non-`Copy`
5166                        // binding used as a field value (`Pair { left: tag,
5167                        // right: tag }`) is moved by the first field and would be
5168                        // `E0382` at the second. Route through `emit_call_arg`
5169                        // (borrow=false) so the same `arg_needs_clone` / reused-fn
5170                        // borrow logic that guards call args also clones a reused
5171                        // field value here. (`emit_expr` alone bypassed it.)
5172                        self.emit_call_arg(val, false)?;
5173                    } else {
5174                        self.buf.push_str(&fname);
5175                    }
5176                }
5177                if let Some(sp) = spread {
5178                    if !fields.is_empty() {
5179                        self.buf.push_str(", ");
5180                    }
5181                    self.buf.push_str("..");
5182                    self.emit_expr(sp)?;
5183                }
5184                self.buf.push_str(" }");
5185                Ok(())
5186            }
5187            NodeKind::ListLiteral { elems } => {
5188                self.buf.push_str("vec![");
5189                for (i, e) in elems.iter().enumerate() {
5190                    if i > 0 {
5191                        self.buf.push_str(", ");
5192                    }
5193                    self.emit_expr(e)?;
5194                }
5195                self.buf.push(']');
5196                Ok(())
5197            }
5198            NodeKind::MapLiteral { entries } => {
5199                if entries.is_empty() {
5200                    self.buf.push_str("std::collections::HashMap::new()");
5201                } else {
5202                    self.buf.push_str("std::collections::HashMap::from([");
5203                    for (i, entry) in entries.iter().enumerate() {
5204                        if i > 0 {
5205                            self.buf.push_str(", ");
5206                        }
5207                        self.buf.push('(');
5208                        self.emit_expr(&entry.key)?;
5209                        self.buf.push_str(", ");
5210                        self.emit_expr(&entry.value)?;
5211                        self.buf.push(')');
5212                    }
5213                    self.buf.push_str("])");
5214                }
5215                Ok(())
5216            }
5217            NodeKind::SetLiteral { elems } => {
5218                if elems.is_empty() {
5219                    self.buf.push_str("std::collections::HashSet::new()");
5220                } else {
5221                    self.buf.push_str("std::collections::HashSet::from([");
5222                    for (i, e) in elems.iter().enumerate() {
5223                        if i > 0 {
5224                            self.buf.push_str(", ");
5225                        }
5226                        self.emit_expr(e)?;
5227                    }
5228                    self.buf.push_str("])");
5229                }
5230                Ok(())
5231            }
5232            NodeKind::TupleLiteral { elems } => {
5233                self.buf.push('(');
5234                for (i, e) in elems.iter().enumerate() {
5235                    if i > 0 {
5236                        self.buf.push_str(", ");
5237                    }
5238                    self.emit_expr(e)?;
5239                }
5240                if elems.len() == 1 {
5241                    self.buf.push(',');
5242                }
5243                self.buf.push(')');
5244                Ok(())
5245            }
5246            NodeKind::Interpolation { parts } => {
5247                // `format!("...", args)`
5248                self.buf.push_str("format!(\"");
5249                let mut format_args: Vec<String> = Vec::new();
5250                for part in parts {
5251                    match part {
5252                        AirInterpolationPart::Literal(s) => {
5253                            self.buf.push_str(&escape_format_string(s));
5254                        }
5255                        AirInterpolationPart::Expr(expr) => {
5256                            // A `Vec`/`HashMap`/`HashSet` has no `Display`, so an
5257                            // interpolated collection value (a collection-typed
5258                            // binding, or a `.keys()`/list-literal expression)
5259                            // uses the `Debug` formatter (`{:?}`). See
5260                            // `collection_bindings` / `expr_is_collection_valued`.
5261                            if self.expr_interpolates_collection(expr) {
5262                                self.buf.push_str("{:?}");
5263                            } else {
5264                                self.buf.push_str("{}");
5265                            }
5266                            let mut sub = RsEmitCtx::new();
5267                            sub.indent = self.indent;
5268                            // The sub-context must see the same enum-variant
5269                            // registry so an interpolated variant construction
5270                            // (`${label(Red)}`) is qualified `Color::Red` too,
5271                            // and the `self_operand_methods` set so an
5272                            // interpolated `${a.compare(b)}` borrows its operand.
5273                            sub.enum_variants = self.enum_variants.clone();
5274                            sub.self_operand_methods = self.self_operand_methods.clone();
5275                            // §10.2/§10.4: an effect op invoked inside an
5276                            // interpolation (`"at ${now()}"`) must be rewritten
5277                            // to its handler call (`__clock.now()`) just like one
5278                            // in statement position. The sub-context therefore
5279                            // needs the effect-op registry, the in-scope handler
5280                            // vars, and the fn→effects / composite-effects maps
5281                            // that drive the rewrite (rs.rs `rewrite_effect_op`).
5282                            // Without these the op emits bare and rustc fails
5283                            // with E0425. The other 4 backends emit interpolated
5284                            // exprs on `self`, so they carry this state already.
5285                            sub.effect_ops = self.effect_ops.clone();
5286                            sub.current_handler_vars = self.current_handler_vars.clone();
5287                            sub.borrowed_handler_effects = self.borrowed_handler_effects.clone();
5288                            sub.fn_effects = self.fn_effects.clone();
5289                            sub.composite_effects = self.composite_effects.clone();
5290                            // Carry the move-reuse clone sets so an interpolated
5291                            // by-value pass of a reused binding still clones.
5292                            sub.reused_let_bindings = self.reused_let_bindings.clone();
5293                            sub.reused_match_bindings = self.reused_match_bindings.clone();
5294                            sub.emit_expr(expr)?;
5295                            format_args.push(sub.buf);
5296                        }
5297                    }
5298                }
5299                self.buf.push('"');
5300                for arg in format_args {
5301                    self.buf.push_str(", ");
5302                    self.buf.push_str(&arg);
5303                }
5304                self.buf.push(')');
5305                Ok(())
5306            }
5307            NodeKind::Placeholder => {
5308                self.buf.push('_');
5309                Ok(())
5310            }
5311            NodeKind::Unreachable => {
5312                self.buf.push_str("unreachable!()");
5313                Ok(())
5314            }
5315            NodeKind::ResultConstruct { variant, value } => {
5316                match variant {
5317                    ResultVariant::Ok => {
5318                        self.buf.push_str("Ok(");
5319                        if let Some(v) = value {
5320                            self.emit_expr(v)?;
5321                        } else {
5322                            self.buf.push_str("()");
5323                        }
5324                        self.buf.push(')');
5325                    }
5326                    ResultVariant::Err => {
5327                        self.buf.push_str("Err(");
5328                        if let Some(v) = value {
5329                            self.emit_expr(v)?;
5330                        } else {
5331                            self.buf.push_str("()");
5332                        }
5333                        self.buf.push(')');
5334                    }
5335                }
5336                Ok(())
5337            }
5338            NodeKind::Assign { op, target, value } => {
5339                // The target is a place expression; suppress the clone-self
5340                // `self.field` → `self.field.clone()` rewrite while emitting it.
5341                let prev_assign_target = self.in_assign_target;
5342                self.in_assign_target = true;
5343                let target_res = self.emit_expr(target);
5344                self.in_assign_target = prev_assign_target;
5345                target_res?;
5346                let op_str = match op {
5347                    AssignOp::Assign => " = ",
5348                    AssignOp::AddAssign => " += ",
5349                    AssignOp::SubAssign => " -= ",
5350                    AssignOp::MulAssign => " *= ",
5351                    AssignOp::DivAssign => " /= ",
5352                    AssignOp::RemAssign => " %= ",
5353                };
5354                self.buf.push_str(op_str);
5355                self.emit_expr(value)?;
5356                Ok(())
5357            }
5358            NodeKind::If {
5359                condition,
5360                then_block,
5361                else_block,
5362                ..
5363            } => {
5364                // If in expression position.
5365                self.buf.push_str("if ");
5366                self.emit_expr(condition)?;
5367                self.buf.push_str(" { ");
5368                self.emit_block_as_expr(then_block)?;
5369                self.buf.push_str(" } else { ");
5370                if let Some(eb) = else_block {
5371                    self.emit_block_as_expr(eb)?;
5372                } else {
5373                    self.buf.push_str("()");
5374                }
5375                self.buf.push_str(" }");
5376                Ok(())
5377            }
5378            NodeKind::Block { stmts, tail } => {
5379                if stmts.is_empty() {
5380                    if let Some(t) = tail {
5381                        return self.emit_tail_value(t);
5382                    }
5383                }
5384                // Block in expression position: `{ stmts; tail }`
5385                self.buf.push_str("{\n");
5386                self.indent += 1;
5387                for s in stmts {
5388                    self.emit_node(s)?;
5389                }
5390                if let Some(t) = tail {
5391                    self.write_indent();
5392                    self.emit_tail_value(t)?;
5393                    self.buf.push('\n');
5394                }
5395                self.indent -= 1;
5396                self.write_indent();
5397                self.buf.push('}');
5398                Ok(())
5399            }
5400            NodeKind::Match { scrutinee, arms } => {
5401                // Match in expression position. Mirror `emit_match`: the
5402                // scrutinee prefix (`.as_slice()` / `.as_str()`) and the mixed
5403                // string-literal/bind re-bind set are computed identically by the
5404                // shared `emit_match_scrutinee_prefix`.
5405                self.buf.push_str("match ");
5406                let prev_rebind = self.emit_match_scrutinee_prefix(scrutinee, arms)?;
5407                self.buf.push_str(" {\n");
5408                self.indent += 1;
5409                for arm in arms {
5410                    self.emit_match_arm(arm)?;
5411                }
5412                self.indent -= 1;
5413                self.write_indent();
5414                self.buf.push('}');
5415                self.str_rebind_match_binds = prev_rebind;
5416                Ok(())
5417            }
5418            // Ownership nodes: direct mapping to Rust.
5419            NodeKind::Move { expr } => {
5420                // Move semantics are default in Rust, just emit the expression.
5421                self.emit_expr(expr)
5422            }
5423            NodeKind::Borrow { expr } => {
5424                self.buf.push('&');
5425                self.emit_expr(expr)?;
5426                Ok(())
5427            }
5428            NodeKind::MutableBorrow { expr } => {
5429                self.buf.push_str("&mut ");
5430                self.emit_expr(expr)?;
5431                Ok(())
5432            }
5433            // Effect operation invocation.
5434            NodeKind::EffectOp {
5435                effect,
5436                operation,
5437                args,
5438            } => {
5439                let effect_name = effect.segments.last().map_or("effect", |s| s.name.as_str());
5440                let _ = write!(
5441                    self.buf,
5442                    "{}.{}",
5443                    to_snake_case(effect_name),
5444                    to_snake_case(&operation.name)
5445                );
5446                self.buf.push('(');
5447                for (i, arg) in args.iter().enumerate() {
5448                    if i > 0 {
5449                        self.buf.push_str(", ");
5450                    }
5451                    self.emit_expr(&arg.value)?;
5452                }
5453                self.buf.push(')');
5454                Ok(())
5455            }
5456            // Type expressions in expression context.
5457            NodeKind::TypeNamed { .. }
5458            | NodeKind::TypeTuple { .. }
5459            | NodeKind::TypeFunction { .. }
5460            | NodeKind::TypeOptional { .. }
5461            | NodeKind::TypeSelf => {
5462                self.buf.push_str("/* type */");
5463                Ok(())
5464            }
5465            NodeKind::EffectRef { path } => {
5466                let name = path
5467                    .segments
5468                    .iter()
5469                    .map(|s| s.name.as_str())
5470                    .collect::<Vec<_>>()
5471                    .join("::");
5472                self.buf.push_str(&name);
5473                Ok(())
5474            }
5475            NodeKind::Error => {
5476                self.buf.push_str("/* error */");
5477                Ok(())
5478            }
5479            _ => {
5480                self.buf.push_str("/* unsupported */");
5481                Ok(())
5482            }
5483        }
5484    }
5485
5486    // ── Match ───────────────────────────────────────────────────────────────
5487
5488    /// Collect the snake-cased binding names a pattern introduces (`Some(x)` →
5489    /// `["x"]`, `Pair(a, b)` → `["a", "b"]`). Used to seed the move-reuse clone
5490    /// analysis: a binding the arm body uses more than once must clone on each
5491    /// by-value use after the first (see `reused_match_bindings`).
5492    fn collect_pattern_binding_names(pat: &AIRNode, out: &mut Vec<String>) {
5493        match &pat.kind {
5494            NodeKind::BindPat { name, .. } => out.push(to_snake_case(&name.name)),
5495            NodeKind::ConstructorPat { fields, .. } => {
5496                for e in fields {
5497                    Self::collect_pattern_binding_names(e, out);
5498                }
5499            }
5500            NodeKind::TuplePat { elems } => {
5501                for e in elems {
5502                    Self::collect_pattern_binding_names(e, out);
5503                }
5504            }
5505            NodeKind::ListPat { elems, rest } => {
5506                for e in elems {
5507                    Self::collect_pattern_binding_names(e, out);
5508                }
5509                if let Some(r) = rest {
5510                    Self::collect_pattern_binding_names(r, out);
5511                }
5512            }
5513            NodeKind::RecordPat { fields, .. } => {
5514                for f in fields {
5515                    if let Some(p) = &f.pattern {
5516                        Self::collect_pattern_binding_names(p, out);
5517                    } else {
5518                        // Shorthand `{ name }` binds `name`.
5519                        out.push(to_snake_case(&f.name.name));
5520                    }
5521                }
5522            }
5523            _ => {}
5524        }
5525    }
5526
5527    /// Count how many times the snake-cased identifier `name` is read inside
5528    /// `node`. A binding read more than once is move-reused (the Rust pattern
5529    /// binds by value, so the first by-value consumer moves it). Counts every
5530    /// `Identifier` occurrence; over-counting only ever adds a harmless clone.
5531    fn count_identifier_uses(node: &AIRNode, name: &str) -> usize {
5532        struct UseCounter<'a> {
5533            name: &'a str,
5534            count: usize,
5535        }
5536        impl bock_air::visitor::Visitor for UseCounter<'_> {
5537            fn visit_node(&mut self, node: &AIRNode) {
5538                if let NodeKind::Identifier { name } = &node.kind {
5539                    if to_snake_case(&name.name) == self.name {
5540                        self.count += 1;
5541                    }
5542                }
5543                bock_air::visitor::walk_node(self, node);
5544            }
5545        }
5546        let mut c = UseCounter { name, count: 0 };
5547        bock_air::visitor::Visitor::visit_node(&mut c, node);
5548        c.count
5549    }
5550
5551    /// Collect the snake-cased names of every `Identifier` read in `node` (used
5552    /// to find a closure body's captured bindings — see the `Lambda` arm).
5553    fn collect_identifier_names(node: &AIRNode, out: &mut Vec<String>) {
5554        struct NameCollector<'a> {
5555            out: &'a mut Vec<String>,
5556        }
5557        impl bock_air::visitor::Visitor for NameCollector<'_> {
5558            fn visit_node(&mut self, node: &AIRNode) {
5559                if let NodeKind::Identifier { name } = &node.kind {
5560                    self.out.push(to_snake_case(&name.name));
5561                }
5562                bock_air::visitor::walk_node(self, node);
5563            }
5564        }
5565        let mut c = NameCollector { out };
5566        bock_air::visitor::Visitor::visit_node(&mut c, node);
5567    }
5568
5569    /// Seed [`Self::reused_let_bindings`] with by-value, non-`Copy` parameters
5570    /// that the body reads more than once. A Bock parameter is passed by value
5571    /// (Rust takes ownership); a non-`Copy` value is *moved* by its first
5572    /// by-value consumer, so a later by-value pass of the same parameter is a
5573    /// use-after-move (`E0382`) — e.g. `show(op, …)` calling `eval(op, …)` then
5574    /// `format_expr(op, …)` where `op: Op` is a (non-`Copy`) generated enum. By
5575    /// registering such parameters here, the call-arg emitter inserts `.clone()`
5576    /// on each by-value pass (every derived type is `Clone`). Returns the names
5577    /// added so the caller can restore the set afterward.
5578    ///
5579    /// `Copy` scalar parameters (`Int`/`Float`/`Bool`/`Char` → `i64`/`f64`/`bool`
5580    /// /`char`) and reference-bound `self`-operands are skipped: they are not
5581    /// moved, and cloning them would be needless (`clippy::clone_on_copy`). The
5582    /// leading `self` receiver is borrowed, never owned, so it is skipped too.
5583    ///
5584    /// `generic_param_names` is the set of the enclosing function's generic type
5585    /// parameter names (`T`, `U`, …). A reused param whose declared type is one of
5586    /// them is recorded in `reused_let_bindings` (so a by-value call pass still
5587    /// clones — that path already requires a `Clone` bound the codegen
5588    /// synthesizes) but **not** in [`Self::reused_value_tail_bindings`]: a generic
5589    /// `T` used as a bare block-tail value (`max_of<T: Ord>`'s `{ a }`) is its
5590    /// last use and must not be `.clone()`d there (`T` may lack `Clone` — E0599).
5591    fn seed_reused_params(
5592        &mut self,
5593        params: &[AIRNode],
5594        body: &AIRNode,
5595        generic_param_names: &std::collections::HashSet<String>,
5596    ) -> Vec<String> {
5597        let mut added = Vec::new();
5598        for p in params {
5599            // Skip the `self` receiver — it lowers to `&self`/`&mut self`, never
5600            // an owned move.
5601            if crate::generator::param_binds_self(p).is_some() {
5602                continue;
5603            }
5604            let NodeKind::Param { pattern, ty, .. } = &p.kind else {
5605                continue;
5606            };
5607            // `Copy` scalars are never moved; cloning them is needless noise.
5608            if ty.as_deref().is_some_and(Self::ast_type_is_copy) {
5609                continue;
5610            }
5611            let NodeKind::BindPat { name, .. } = &pattern.kind else {
5612                continue;
5613            };
5614            let rs_name = to_snake_case(&name.name);
5615            // A param is move-reused if read by value more than once, OR read
5616            // even once *inside a loop body* — the loop re-executes that read on
5617            // each iteration, so the value is moved on the first pass and gone on
5618            // the second (`category_total`'s `cat` in `for e in …`). A single use
5619            // outside any loop is a one-shot move and needs no clone.
5620            if (Self::count_identifier_uses(body, &rs_name) > 1
5621                || Self::identifier_used_in_loop(body, &rs_name))
5622                && self.reused_let_bindings.insert(rs_name.clone())
5623            {
5624                // A concrete (non-generic) type is `Clone`, so it is safe to clone
5625                // in a bare value/block-tail position. A generic `T` is not (no
5626                // `Clone` bound) — exclude it from the tail-clone set.
5627                if !Self::ast_type_is_generic_param(ty.as_deref(), generic_param_names) {
5628                    self.reused_value_tail_bindings.insert(rs_name.clone());
5629                }
5630                added.push(rs_name);
5631            }
5632        }
5633        added
5634    }
5635
5636    /// True when `ty` is a bare reference to one of `generic_param_names` — a
5637    /// `TypeNamed` with a single, argument-less segment matching a declared
5638    /// generic type parameter (`T`, `U`). Such a type has no `Clone` bound unless
5639    /// the codegen synthesized one, so it must not be cloned in a bare
5640    /// value/tail position (see [`Self::seed_reused_params`]).
5641    fn ast_type_is_generic_param(
5642        ty: Option<&AIRNode>,
5643        generic_param_names: &std::collections::HashSet<String>,
5644    ) -> bool {
5645        matches!(
5646            ty.map(|t| &t.kind),
5647            Some(NodeKind::TypeNamed { path, args })
5648                if args.is_empty()
5649                    && path.segments.len() == 1
5650                    && path
5651                        .segments
5652                        .last()
5653                        .is_some_and(|s| generic_param_names.contains(&s.name))
5654        )
5655    }
5656
5657    /// The set of generic type-parameter names declared by a function
5658    /// (`fn f[T, U](..)` → `{T, U}`). Used to keep a generic `T` value out of the
5659    /// bare-tail clone set (see [`Self::seed_reused_params`]).
5660    fn generic_param_name_set(
5661        generic_params: &[bock_ast::GenericParam],
5662    ) -> std::collections::HashSet<String> {
5663        generic_params
5664            .iter()
5665            .map(|gp| gp.name.name.clone())
5666            .collect()
5667    }
5668
5669    /// True when `name` is read anywhere inside a loop body (`for`/`while`/
5670    /// `loop`) within `node`. A binding consumed by value there is moved on the
5671    /// first iteration and unavailable on the next (`E0382`), so it must be
5672    /// cloned at each by-value pass — see [`Self::seed_reused_params`].
5673    fn identifier_used_in_loop(node: &AIRNode, name: &str) -> bool {
5674        struct LoopUseScan<'a> {
5675            name: &'a str,
5676            in_loop: usize,
5677            found: bool,
5678        }
5679        impl bock_air::visitor::Visitor for LoopUseScan<'_> {
5680            fn visit_node(&mut self, node: &AIRNode) {
5681                match &node.kind {
5682                    NodeKind::For { iterable, body, .. } => {
5683                        // A NESTED loop's iterable IS re-executed once per
5684                        // surrounding-loop iteration (`for n in nums` inside
5685                        // `for tag in tags`), so a binding used only as that
5686                        // iterable is still move-reused across outer iterations
5687                        // — scan it when already inside a loop. The OUTERMOST
5688                        // loop's iterable runs exactly once, so it is not counted
5689                        // (skipped while `in_loop == 0`).
5690                        if self.in_loop > 0 {
5691                            bock_air::visitor::Visitor::visit_node(self, iterable);
5692                        }
5693                        self.in_loop += 1;
5694                        bock_air::visitor::Visitor::visit_node(self, body);
5695                        self.in_loop -= 1;
5696                    }
5697                    NodeKind::While { condition, body } => {
5698                        // As above for `while`'s condition: a nested `while`
5699                        // re-evaluates its condition every outer iteration.
5700                        if self.in_loop > 0 {
5701                            bock_air::visitor::Visitor::visit_node(self, condition);
5702                        }
5703                        self.in_loop += 1;
5704                        bock_air::visitor::Visitor::visit_node(self, body);
5705                        self.in_loop -= 1;
5706                    }
5707                    NodeKind::Loop { body, .. } => {
5708                        self.in_loop += 1;
5709                        bock_air::visitor::Visitor::visit_node(self, body);
5710                        self.in_loop -= 1;
5711                    }
5712                    NodeKind::Identifier { name } => {
5713                        if self.in_loop > 0 && to_snake_case(&name.name) == self.name {
5714                            self.found = true;
5715                        }
5716                    }
5717                    _ => bock_air::visitor::walk_node(self, node),
5718                }
5719            }
5720        }
5721        let mut s = LoopUseScan {
5722            name,
5723            in_loop: 0,
5724            found: false,
5725        };
5726        bock_air::visitor::Visitor::visit_node(&mut s, node);
5727        s.found
5728    }
5729
5730    /// Decide whether a `**` (`BinOp::Pow`) lowers to the float path
5731    /// (`f64::powf`) or the int path (`i64::pow`). Returns `true` when either
5732    /// operand is a statically-`Float` expression — currently a `Float` literal
5733    /// (`b ** 3.0`, `2.0 ** n`). The rust backend has no full local
5734    /// type-inference environment, so an unannotated `Float`-typed binding on
5735    /// both sides falls back to the int path; the common cases (`2 ** 10` int,
5736    /// `x ** 2.0` float) are covered by the literal probe. Choosing the int path
5737    /// when unsure keeps exact integer precision, matching the go backend.
5738    fn pow_is_float(left: &AIRNode, right: &AIRNode) -> bool {
5739        Self::expr_is_float_literal(left) || Self::expr_is_float_literal(right)
5740    }
5741
5742    /// True when `node` is (syntactically) a `Float` literal, looking through a
5743    /// unary negation (`-2.0`). Used to route `**` lowering — see
5744    /// [`Self::pow_is_float`].
5745    fn expr_is_float_literal(node: &AIRNode) -> bool {
5746        match &node.kind {
5747            NodeKind::Literal {
5748                lit: Literal::Float(_),
5749            } => true,
5750            NodeKind::UnaryOp {
5751                op: UnaryOp::Neg,
5752                operand,
5753            } => Self::expr_is_float_literal(operand),
5754            _ => false,
5755        }
5756    }
5757
5758    /// True when an AIR type node is a `List`/`Map`/`Set` — a Rust
5759    /// `Vec`/`HashMap`/`HashSet`, none of which implement `std::fmt::Display`.
5760    /// A binding of such a type interpolated into a string must use the `Debug`
5761    /// formatter. See [`Self::collection_bindings`].
5762    fn type_is_display_collection(ty: &AIRNode) -> bool {
5763        matches!(
5764            &ty.kind,
5765            NodeKind::TypeNamed { path, .. }
5766                if path.segments.last().is_some_and(|s|
5767                    matches!(s.name.as_str(), "List" | "Map" | "Set"))
5768        )
5769    }
5770
5771    /// True when `value` (a `let` RHS) syntactically evaluates to a Rust
5772    /// collection (`Vec`/`HashMap`/`HashSet`) — a list/map/set literal, a list
5773    /// concatenation (`a + [..]`), or a collection-returning desugared method
5774    /// (`.keys()`/`.values()`/`.entries()`/`.to_list()`). Used to mark the
5775    /// binding for `{:?}` interpolation (a collection has no `Display`). A
5776    /// best-effort syntactic probe — when in doubt it returns `false` (the
5777    /// binding keeps the default `{}`, which is correct for non-collections).
5778    fn expr_is_collection_valued(value: &AIRNode) -> bool {
5779        match &value.kind {
5780            NodeKind::ListLiteral { .. }
5781            | NodeKind::MapLiteral { .. }
5782            | NodeKind::SetLiteral { .. } => true,
5783            // `a + [..]` / `[..] + b` — a list concatenation is still a `Vec`.
5784            NodeKind::BinaryOp {
5785                op: BinOp::Add,
5786                left,
5787                right,
5788            } => Self::expr_is_collection_valued(left) || Self::expr_is_collection_valued(right),
5789            // A desugared collection-returning method (`map.keys()` etc.). The
5790            // method name is the trailing field of the callee.
5791            NodeKind::Call { callee, .. } => {
5792                if let NodeKind::FieldAccess { field, .. } = &callee.kind {
5793                    matches!(
5794                        field.name.as_str(),
5795                        "keys" | "values" | "entries" | "to_list"
5796                    )
5797                } else {
5798                    false
5799                }
5800            }
5801            NodeKind::MethodCall { method, .. } => matches!(
5802                method.name.as_str(),
5803                "keys" | "values" | "entries" | "to_list"
5804            ),
5805            _ => false,
5806        }
5807    }
5808
5809    /// True when an interpolated expression evaluates to a Rust collection
5810    /// (`Vec`/`HashMap`/`HashSet`) — either a bare identifier naming a tracked
5811    /// [`Self::collection_bindings`] binding (`${keys}`) or a directly
5812    /// collection-valued expression (`${map.keys()}`, `${[1, 2]}`). Such a value
5813    /// formats with `{:?}`, not `{}` (collections have no `Display` — E0277).
5814    fn expr_interpolates_collection(&self, expr: &AIRNode) -> bool {
5815        if let NodeKind::Identifier { name } = &expr.kind {
5816            if self
5817                .collection_bindings
5818                .contains(&to_snake_case(&name.name))
5819            {
5820                return true;
5821            }
5822        }
5823        Self::expr_is_collection_valued(expr)
5824    }
5825
5826    /// True when an AIR type node lowers to a `Copy` Rust scalar
5827    /// (`Int`/`Float`/`Bool`/`Char` → `i64`/`f64`/`bool`/`char`). Such a value is
5828    /// never moved by a by-value use, so it needs no move-reuse clone.
5829    /// Conservative: anything else (String, records, enums, containers, optionals,
5830    /// tuples, functions, generic type vars) is treated as non-`Copy`.
5831    fn ast_type_is_copy(ty: &AIRNode) -> bool {
5832        match &ty.kind {
5833            NodeKind::TypeNamed { path, args } => {
5834                args.is_empty()
5835                    && path.segments.last().is_some_and(|s| {
5836                        matches!(s.name.as_str(), "Int" | "Float" | "Bool" | "Char")
5837                    })
5838            }
5839            _ => false,
5840        }
5841    }
5842
5843    /// True when `arg` is a bare identifier naming a match binding the current
5844    /// arm reuses ([`Self::reused_match_bindings`]) — a by-value pass of it
5845    /// after an earlier by-value consumer would move an already-moved value
5846    /// (`E0382`). The caller emits `<arg>.clone()` instead of `<arg>` for such
5847    /// args. Bare identifiers only: a non-identifier expression (`f(x)`,
5848    /// `x + 1`) produces a fresh value with no move hazard.
5849    fn arg_is_reused_binding(&self, arg: &AIRNode) -> bool {
5850        match &arg.kind {
5851            NodeKind::Identifier { name } => {
5852                let snake = to_snake_case(&name.name);
5853                self.reused_match_bindings.contains(&snake)
5854                    || self.reused_let_bindings.contains(&snake)
5855            }
5856            _ => false,
5857        }
5858    }
5859
5860    /// Emit an expression in **callee** position, parenthesizing it when its
5861    /// surface syntax would otherwise swallow the trailing argument list.
5862    ///
5863    /// The case that matters is a bare closure callee: `|x| body` followed by
5864    /// `(arg)` parses in Rust as `|x| (body(arg))` — the call binds to the body,
5865    /// never invoking the closure. Wrapping it as `(|x| body)(arg)` makes the
5866    /// call apply to the closure itself. This arises when the AIR compose
5867    /// desugar (`f >> g` → `(__compose_x) => g(f(__compose_x))`) **nests**:
5868    /// chained `>>` lowers the inner compose to a `Lambda` (or a `Compose` still
5869    /// awaiting lowering), which then appears as the callee `f` inside
5870    /// `f(__compose_x)`. Mirrors the python backend's `emit_callee`.
5871    fn emit_callee_rs(&mut self, callee: &AIRNode) -> Result<(), CodegenError> {
5872        if matches!(
5873            callee.kind,
5874            NodeKind::Lambda { .. } | NodeKind::Compose { .. }
5875        ) {
5876            self.buf.push('(');
5877            self.emit_expr(callee)?;
5878            self.buf.push(')');
5879            Ok(())
5880        } else {
5881            self.emit_expr(callee)
5882        }
5883    }
5884
5885    /// True when a by-value call argument must be cloned to keep an earlier
5886    /// value live (`E0382`). Extends [`Self::arg_is_reused_binding`] (the bare
5887    /// reused-binding case) with the **record-field** case: passing
5888    /// `owner.field` by value moves the field out of `owner`, partially moving
5889    /// it; if `owner` is itself a move-reused binding (read again afterward — a
5890    /// later `owner.other`, `owner.method()`, or the same `owner.field` again),
5891    /// that later read is a use-after-(partial-)move. Cloning the field at the
5892    /// pass site leaves `owner` intact (every generated record derives `Clone`),
5893    /// mirroring [`Self::iterable_is_reused`] for `for`-iterables. A non-reused
5894    /// owner (a fresh value, a one-shot local) needs no clone: the single move is
5895    /// fine. Only a one-level `<ident>.<field>` is handled; deeper chains
5896    /// (`a.b.c`) are rare in v1 and fall through to the no-clone path.
5897    fn arg_needs_clone(&self, arg: &AIRNode) -> bool {
5898        if self.arg_is_reused_binding(arg) {
5899            return true;
5900        }
5901        if let NodeKind::FieldAccess { object, .. } = &arg.kind {
5902            // `owner.field` clones when `owner` is a reused binding: the field
5903            // move would partially move `owner`, breaking a later read of it.
5904            return self.arg_is_reused_binding(object);
5905        }
5906        false
5907    }
5908
5909    /// True when `arg` is a bare identifier naming a function/closure-valued
5910    /// binding ([`Self::fn_typed_bindings`]) that is reused
5911    /// ([`Self::reused_let_bindings`]). Such a binding holds an `impl Fn` opaque
5912    /// value, which is **not** `Clone` (E0599) — so a move-reuse pass must
5913    /// *borrow* it (`&f`) rather than clone it. `&F` satisfies an `impl Fn`
5914    /// parameter when `F: Fn`, and the borrow leaves the binding live for the
5915    /// next pass.
5916    fn arg_is_reused_fn_binding(&self, arg: &AIRNode) -> bool {
5917        if let NodeKind::Identifier { name } = &arg.kind {
5918            let snake = to_snake_case(&name.name);
5919            return self.fn_typed_bindings.contains(&snake)
5920                && self.reused_let_bindings.contains(&snake);
5921        }
5922        false
5923    }
5924
5925    /// True when a block's **tail expression** is a bare identifier naming a
5926    /// move-reused, non-`Copy` binding ([`Self::reused_let_bindings`]) and so
5927    /// must be `.clone()`d to keep an earlier or sibling use live (`E0382`).
5928    ///
5929    /// The case that motivates this: a non-`Copy` parameter (`value: String`)
5930    /// read as the *value* of several sibling `if`/`else` arms —
5931    /// `let a = if (..) { value } else { .. }; let b = if (..) { value } else
5932    /// { .. }`. Each `{ value }` arm-tail moves the parameter, so the second arm
5933    /// is a use-after-move. The call-argument emitter ([`Self::emit_call_arg`])
5934    /// already clones such a binding when it is *passed to a call*; a bare
5935    /// arm-tail (or block-tail) value is the remaining position where the same
5936    /// move hazard appears. Cloning is always sound — every generated value type
5937    /// is `Clone`.
5938    ///
5939    /// A function/closure-valued binding ([`Self::fn_typed_bindings`]) is
5940    /// excluded: an `impl Fn` opaque type is not `Clone` (E0599); a move-reuse of
5941    /// one is handled by the borrow path elsewhere. Only a bare identifier
5942    /// qualifies — any other tail expression (a call, an interpolation, a
5943    /// constructor) produces a fresh value with no move hazard.
5944    fn tail_ident_needs_clone(&self, tail: &AIRNode) -> bool {
5945        if let NodeKind::Identifier { name } = &tail.kind {
5946            let snake = to_snake_case(&name.name);
5947            return self.reused_value_tail_bindings.contains(&snake)
5948                && !self.fn_typed_bindings.contains(&snake);
5949        }
5950        false
5951    }
5952
5953    /// Emit a single positional call argument, applying the move-reuse fixups a
5954    /// by-value Rust call needs to honour Bock's value semantics:
5955    ///
5956    /// - `borrow` (the `Self`-operand-trait case): pass by shared reference
5957    ///   (`&arg`) so the caller can keep using the value.
5958    /// - a reused function/closure binding: borrow it (`&f`) — an `impl Fn` is
5959    ///   not `Clone`, but `&F: Fn` ([`Self::arg_is_reused_fn_binding`]).
5960    /// - a reused non-`Copy` binding or a record field of a reused owner: clone
5961    ///   it (`arg.clone()`) so a later use stays live
5962    ///   ([`Self::arg_needs_clone`]).
5963    fn emit_call_arg(&mut self, arg: &AIRNode, borrow: bool) -> Result<(), CodegenError> {
5964        if borrow {
5965            self.buf.push('&');
5966            self.emit_expr(arg)?;
5967            return Ok(());
5968        }
5969        if self.arg_is_reused_fn_binding(arg) {
5970            self.buf.push('&');
5971            self.emit_expr(arg)?;
5972            return Ok(());
5973        }
5974        let clone_reused = self.arg_needs_clone(arg);
5975        self.emit_expr(arg)?;
5976        if clone_reused {
5977            self.buf.push_str(".clone()");
5978        }
5979        Ok(())
5980    }
5981
5982    /// True when a `for` iterable should be cloned to avoid moving (or partially
5983    /// moving) a binding that is reused after the loop. Covers a bare reused
5984    /// binding (`for n in nodes` with `nodes` used again — [`Self::arg_is_reused_binding`])
5985    /// *and* a field access of one (`for row in dataset.rows` with `dataset`
5986    /// reused: iterating the field partially moves the owner, so a later use of
5987    /// `dataset` is `E0382`). A field access is cloned when its root binding is
5988    /// reused.
5989    fn iterable_is_reused(&self, iterable: &AIRNode) -> bool {
5990        if self.arg_is_reused_binding(iterable) {
5991            return true;
5992        }
5993        if let NodeKind::FieldAccess { object, .. } = &iterable.kind {
5994            return self.arg_is_reused_binding(object);
5995        }
5996        false
5997    }
5998
5999    fn emit_match(&mut self, scrutinee: &AIRNode, arms: &[AIRNode]) -> Result<(), CodegenError> {
6000        let ind = self.indent_str();
6001        let _ = write!(self.buf, "{ind}match ");
6002        // The scrutinee prefix (`.as_slice()` for list-pattern arms, `.as_str()`
6003        // for `&str`-literal arms) and the mixed string-literal/bind re-bind set
6004        // are chosen by `emit_match_scrutinee_prefix`.
6005        let prev_rebind = self.emit_match_scrutinee_prefix(scrutinee, arms)?;
6006        self.buf.push_str(" {\n");
6007        self.indent += 1;
6008        for arm in arms {
6009            self.emit_match_arm(arm)?;
6010        }
6011        self.indent -= 1;
6012        self.writeln("}");
6013        self.str_rebind_match_binds = prev_rebind;
6014        Ok(())
6015    }
6016
6017    /// True when `scrutinee` is a move-reused binding (used again after the
6018    /// `match`, so it is in the reuse clone set) AND some arm's pattern moves a
6019    /// field out of it — a record/constructor/tuple pattern that introduces at
6020    /// least one binding. In that case the scrutinee must be matched on a
6021    /// `.clone()` so the original binding survives the partial move (`E0382`).
6022    /// A pattern that binds nothing (bare variant, wildcard, literal,
6023    /// whole-scrutinee bind) moves nothing and needs no clone.
6024    fn match_scrutinee_needs_clone(&self, scrutinee: &AIRNode, arms: &[AIRNode]) -> bool {
6025        if !self.arg_is_reused_binding(scrutinee) {
6026            return false;
6027        }
6028        arms.iter().any(|arm| {
6029            let NodeKind::MatchArm { pattern, .. } = &arm.kind else {
6030                return false;
6031            };
6032            Self::pattern_moves_fields(pattern)
6033        })
6034    }
6035
6036    /// True when `pat` destructures and binds at least one field by value — a
6037    /// record/constructor/tuple/list pattern introducing a `BindPat`. Such a
6038    /// pattern moves the bound (possibly non-`Copy`) field out of the matched
6039    /// value. A whole-value bind (`other => …`, a top-level `BindPat`) is NOT a
6040    /// field move — it rebinds the whole scrutinee, which the existing
6041    /// whole-scrutinee handling already covers — so it does not count here.
6042    fn pattern_moves_fields(pat: &AIRNode) -> bool {
6043        match &pat.kind {
6044            NodeKind::ConstructorPat { .. }
6045            | NodeKind::RecordPat { .. }
6046            | NodeKind::TuplePat { .. }
6047            | NodeKind::ListPat { .. } => {
6048                let mut names = Vec::new();
6049                Self::collect_pattern_binding_names(pat, &mut names);
6050                !names.is_empty()
6051            }
6052            _ => false,
6053        }
6054    }
6055
6056    /// Emit the scrutinee expression for a `match`, choosing the `.as_slice()` /
6057    /// `.as_str()` wrap, and seed [`Self::str_rebind_match_binds`] for a mixed
6058    /// string-literal / whole-scrutinee-bind match. Shared by the statement-form
6059    /// [`Self::emit_match`] and the expression-position `Match` arm so both lower
6060    /// the scrutinee identically.
6061    ///
6062    /// Returns the *previous* `str_rebind_match_binds` set so the caller restores
6063    /// it after emitting the arms (the set is per-`match`, never leaking to a
6064    /// sibling/outer match). The caller writes the surrounding `match`/`{`/`}`.
6065    fn emit_match_scrutinee_prefix(
6066        &mut self,
6067        scrutinee: &AIRNode,
6068        arms: &[AIRNode],
6069    ) -> Result<std::collections::HashSet<String>, CodegenError> {
6070        let prev_rebind = std::mem::take(&mut self.str_rebind_match_binds);
6071        let slice_match = arms.iter().any(Self::arm_matches_list);
6072        // A `String` scrutinee that mixes `&str` literal arms with a
6073        // whole-scrutinee bind (`other => …`) must still match on `.as_str()`
6074        // (the literal arm is `&str`, so a bare `String` scrutinee is E0308), but
6075        // the wrap retypes the bind to `&str`. We re-bind those arms to an owned
6076        // `String` inside their body — see `str_rebind_match_binds`.
6077        let str_literal_match = !slice_match && arms.iter().any(Self::arm_matches_str_literal);
6078        let mixed_bind = str_literal_match && arms.iter().any(Self::arm_binds_scrutinee);
6079        if slice_match {
6080            // Bock list/array values are `Vec<T>` in this backend, but Rust slice
6081            // patterns (`[]`, `[head, ..tail]`) only match `[T]`/`&[T]`, not
6082            // `Vec<T>` (E0529). Match on the scrutinee's `.as_slice()` (`&[T]`):
6083            // default binding modes then bind elements by shared reference, and a
6084            // `rest @ ..` tail binds to a sized `&[T]` (a by-value `[T]` tail
6085            // would be unsized — E0277).
6086            self.buf.push('(');
6087            self.emit_expr(scrutinee)?;
6088            self.buf.push_str(").as_slice()");
6089        } else if str_literal_match {
6090            // `String` scrutinee vs `&str` literal arms → match on `.as_str()`.
6091            // This now also fires when a whole-scrutinee bind is present (the
6092            // `mixed_bind` case): the bind arm is re-bound to `String` below.
6093            self.buf.push('(');
6094            self.emit_expr(scrutinee)?;
6095            self.buf.push_str(").as_str()");
6096            if mixed_bind {
6097                for arm in arms {
6098                    if let NodeKind::MatchArm { pattern, .. } = &arm.kind {
6099                        Self::collect_scrutinee_bind_names(
6100                            pattern,
6101                            &mut self.str_rebind_match_binds,
6102                        );
6103                    }
6104                }
6105            }
6106        } else {
6107            // A record/constructor/tuple pattern arm binds the scrutinee's
6108            // fields BY VALUE, moving non-`Copy` fields out of the scrutinee
6109            // (`match u { User { name, age } => … }` moves the `String` `name`).
6110            // If the scrutinee is a binding reused after the match (`u.name`
6111            // later), matching on `u` directly leaves it partially moved and a
6112            // later read is `E0382`. Match on `u.clone()` so the original
6113            // binding stays intact — analogous to `iterable_is_reused` cloning a
6114            // reused `for` iterable. A pattern that binds nothing (a bare
6115            // variant / wildcard / literal) moves nothing, so no clone.
6116            if self.match_scrutinee_needs_clone(scrutinee, arms) {
6117                self.emit_expr(scrutinee)?;
6118                self.buf.push_str(".clone()");
6119            } else {
6120                self.emit_expr(scrutinee)?;
6121            }
6122        }
6123        Ok(prev_rebind)
6124    }
6125
6126    /// Collect the snake-cased names a top-level whole-scrutinee binding pattern
6127    /// introduces (`other`, or each alternative of `a | b`, or the bind inside a
6128    /// guard pattern), used to drive the `&str` → `String` re-bind in a mixed
6129    /// string-literal / bind match (see [`Self::emit_match`]).
6130    fn collect_scrutinee_bind_names(pat: &AIRNode, out: &mut std::collections::HashSet<String>) {
6131        match &pat.kind {
6132            NodeKind::BindPat { name, .. } => {
6133                out.insert(to_snake_case(&name.name));
6134            }
6135            NodeKind::OrPat { alternatives } => {
6136                for alt in alternatives {
6137                    Self::collect_scrutinee_bind_names(alt, out);
6138                }
6139            }
6140            NodeKind::GuardPat { pattern, .. } => {
6141                Self::collect_scrutinee_bind_names(pattern, out);
6142            }
6143            _ => {}
6144        }
6145    }
6146
6147    /// Emit `let <name> = <name>.to_string();` for each `&str` whole-scrutinee
6148    /// bind that a mixed string-literal / bind `String` match must restore to an
6149    /// owned `String` (Q-rust-str-mixed-binding). Called at the top of the arm
6150    /// body, inside the arm's `{ }` block. `&str::to_string()` always yields a
6151    /// `String`, so the shadowing `let` is sound regardless of how the body uses
6152    /// the binding.
6153    fn emit_str_rebinds(&mut self, names: &[String]) {
6154        for n in names {
6155            self.writeln(&format!("let {n} = {n}.to_string();"));
6156        }
6157    }
6158
6159    /// Whether a match arm's pattern is (or, under `|`/guard, contains) a list
6160    /// pattern — the signal to match on the scrutinee's `.as_slice()`. See
6161    /// [`Self::emit_match`].
6162    fn arm_matches_list(arm: &AIRNode) -> bool {
6163        if let NodeKind::MatchArm { pattern, .. } = &arm.kind {
6164            Self::pattern_is_list(pattern)
6165        } else {
6166            false
6167        }
6168    }
6169
6170    /// Whether `pat` is a list pattern, looking through `|`-alternatives and a
6171    /// trailing pattern guard.
6172    fn pattern_is_list(pat: &AIRNode) -> bool {
6173        match &pat.kind {
6174            NodeKind::ListPat { .. } => true,
6175            NodeKind::OrPat { alternatives } => alternatives.iter().any(Self::pattern_is_list),
6176            NodeKind::GuardPat { pattern, .. } => Self::pattern_is_list(pattern),
6177            _ => false,
6178        }
6179    }
6180
6181    /// Whether a match arm's pattern is (or, under `|`/guard, contains) a string
6182    /// literal pattern — the signal to match on the scrutinee's `.as_str()`. See
6183    /// [`Self::emit_match_scrutinee_prefix`].
6184    fn arm_matches_str_literal(arm: &AIRNode) -> bool {
6185        if let NodeKind::MatchArm { pattern, .. } = &arm.kind {
6186            Self::pattern_is_str_literal(pattern)
6187        } else {
6188            false
6189        }
6190    }
6191
6192    /// Whether `pat` is a string-literal pattern (`"foo"`), looking through
6193    /// `|`-alternatives and a trailing pattern guard.
6194    fn pattern_is_str_literal(pat: &AIRNode) -> bool {
6195        match &pat.kind {
6196            NodeKind::LiteralPat {
6197                lit: Literal::String(_),
6198            } => true,
6199            NodeKind::OrPat { alternatives } => {
6200                alternatives.iter().any(Self::pattern_is_str_literal)
6201            }
6202            NodeKind::GuardPat { pattern, .. } => Self::pattern_is_str_literal(pattern),
6203            _ => false,
6204        }
6205    }
6206
6207    /// Whether an arm's pattern binds the whole scrutinee by name at top level
6208    /// (`other => …`), looking through `|`-alternatives and a trailing guard. When
6209    /// a `.as_str()`-wrapped `String` match also carries such a bind, the bind is
6210    /// `&str` and must be re-bound to an owned `String` in its arm body (see
6211    /// [`Self::emit_match_scrutinee_prefix`]).
6212    fn arm_binds_scrutinee(arm: &AIRNode) -> bool {
6213        if let NodeKind::MatchArm { pattern, .. } = &arm.kind {
6214            Self::pattern_binds_scrutinee(pattern)
6215        } else {
6216            false
6217        }
6218    }
6219
6220    /// Whether `pat` is a top-level binding pattern (`other`), looking through
6221    /// `|`-alternatives and a trailing pattern guard. A wildcard (`_`) does not
6222    /// bind, so it is not counted.
6223    fn pattern_binds_scrutinee(pat: &AIRNode) -> bool {
6224        match &pat.kind {
6225            NodeKind::BindPat { .. } => true,
6226            NodeKind::OrPat { alternatives } => {
6227                alternatives.iter().any(Self::pattern_binds_scrutinee)
6228            }
6229            NodeKind::GuardPat { pattern, .. } => Self::pattern_binds_scrutinee(pattern),
6230            _ => false,
6231        }
6232    }
6233
6234    fn emit_match_arm(&mut self, arm: &AIRNode) -> Result<(), CodegenError> {
6235        if let NodeKind::MatchArm {
6236            pattern,
6237            guard,
6238            body,
6239        } = &arm.kind
6240        {
6241            // Seed the move-reuse clone set for this arm: any pattern binding
6242            // the body reads more than once is moved by its first by-value
6243            // consumer, so later by-value uses must `.clone()` (`E0382`). Scoped
6244            // to the arm (saved/restored) so it never leaks to a sibling/outer
6245            // arm. See `reused_match_bindings`.
6246            let prev_reused = self.reused_match_bindings.clone();
6247            let mut bound = Vec::new();
6248            Self::collect_pattern_binding_names(pattern, &mut bound);
6249            for name in bound {
6250                if Self::count_identifier_uses(body, &name) > 1 {
6251                    self.reused_match_bindings.insert(name);
6252                }
6253            }
6254            let ind = self.indent_str();
6255            let _ = write!(self.buf, "{ind}");
6256            self.emit_pattern(pattern)?;
6257            if let Some(g) = guard {
6258                self.buf.push_str(" if ");
6259                self.emit_expr(g)?;
6260            }
6261            self.buf.push_str(" => ");
6262            // Q-rust-str-mixed-binding: in a `.as_str()`-wrapped `String` match
6263            // that mixes a `&str` literal arm with a whole-scrutinee bind, this
6264            // arm's bind is `&str` but the Bock binding is a `String`. Re-bind it
6265            // to an owned `String` at the top of the arm body. This forces the
6266            // arm into a `{ … }` block (even a one-expression body), so the
6267            // shadowing `let` precedes the body.
6268            let rebinds: Vec<String> = if self.str_rebind_match_binds.is_empty() {
6269                Vec::new()
6270            } else {
6271                let mut names = std::collections::HashSet::new();
6272                Self::collect_scrutinee_bind_names(pattern, &mut names);
6273                let mut v: Vec<String> = names
6274                    .into_iter()
6275                    .filter(|n| self.str_rebind_match_binds.contains(n))
6276                    .collect();
6277                v.sort(); // deterministic emission order
6278                v
6279            };
6280            // A statement-bodied arm (`break`/`continue`/`return`/assignment,
6281            // or a block whose tail is one) has no value. Rust `match` arms
6282            // accept statements directly, so route such a body through the
6283            // statement emitter inside a `{ }` block.
6284            if crate::generator::arm_body_is_statement(body) {
6285                self.buf.push_str("{\n");
6286                self.indent += 1;
6287                self.emit_str_rebinds(&rebinds);
6288                if let NodeKind::Block { .. } = &body.kind {
6289                    self.emit_block_body(body)?;
6290                } else {
6291                    self.emit_stmt(body)?;
6292                }
6293                self.indent -= 1;
6294                self.writeln("}");
6295                self.reused_match_bindings = prev_reused;
6296                return Ok(());
6297            }
6298            // Single-expression body → inline; otherwise block. A re-bind forces
6299            // the block form so the shadowing `let` can precede the value.
6300            if rebinds.is_empty() {
6301                if let NodeKind::Block { stmts, tail } = &body.kind {
6302                    if stmts.is_empty() {
6303                        if let Some(t) = tail {
6304                            self.emit_expr(t)?;
6305                            self.buf.push_str(",\n");
6306                            self.reused_match_bindings = prev_reused;
6307                            return Ok(());
6308                        }
6309                    }
6310                    self.buf.push_str("{\n");
6311                    self.indent += 1;
6312                    self.emit_block_body(body)?;
6313                    self.indent -= 1;
6314                    self.writeln("}");
6315                } else {
6316                    self.emit_expr(body)?;
6317                    self.buf.push_str(",\n");
6318                }
6319            } else {
6320                self.buf.push_str("{\n");
6321                self.indent += 1;
6322                self.emit_str_rebinds(&rebinds);
6323                self.emit_block_body(body)?;
6324                self.indent -= 1;
6325                self.writeln("}");
6326            }
6327            self.reused_match_bindings = prev_reused;
6328        }
6329        Ok(())
6330    }
6331
6332    fn emit_pattern(&mut self, pat: &AIRNode) -> Result<(), CodegenError> {
6333        match &pat.kind {
6334            NodeKind::WildcardPat => {
6335                self.buf.push('_');
6336            }
6337            NodeKind::BindPat { name, is_mut } => {
6338                if *is_mut {
6339                    self.buf.push_str("mut ");
6340                }
6341                self.buf.push_str(&to_snake_case(&name.name));
6342            }
6343            NodeKind::LiteralPat { lit } => match lit {
6344                Literal::Int(s) => {
6345                    self.buf.push_str(s);
6346                    self.buf.push_str("_i64");
6347                }
6348                Literal::Float(s) => self.buf.push_str(s),
6349                Literal::Bool(b) => self.buf.push_str(if *b { "true" } else { "false" }),
6350                Literal::Char(s) => {
6351                    self.buf.push('\'');
6352                    self.buf.push_str(s);
6353                    self.buf.push('\'');
6354                }
6355                Literal::String(s) => {
6356                    self.buf.push('"');
6357                    self.buf.push_str(&escape_rs_string(s));
6358                    self.buf.push('"');
6359                }
6360                Literal::Unit => self.buf.push_str("()"),
6361            },
6362            NodeKind::ConstructorPat { path, fields } => {
6363                // Prelude `Ordering` variant patterns match Rust's native
6364                // `std::cmp::Ordering` (the construction side maps the same way)
6365                // — UNLESS the real `core.compare.Ordering` enum is reachable, in
6366                // which case the user enum (`Ordering::Less`) is matched via the
6367                // qualifier path below.
6368                if let Some(variant) = path
6369                    .segments
6370                    .last()
6371                    .and_then(|s| crate::generator::ordering_variant(&s.name))
6372                {
6373                    if fields.is_empty() && !self.ordering_enum_reachable() {
6374                        let _ = write!(self.buf, "std::cmp::Ordering::{variant}");
6375                        return Ok(());
6376                    }
6377                }
6378                // Qualify a user enum-variant pattern `Enum::Variant`; built-in
6379                // and non-variant paths keep their original `::`-joined form.
6380                let variant_name = if let Some(enum_name) = self.variant_enum_qualifier(path) {
6381                    let variant = path.segments.last().map_or("", |s| s.name.as_str());
6382                    format!("{enum_name}::{variant}")
6383                } else {
6384                    path.segments
6385                        .iter()
6386                        .map(|s| s.name.as_str())
6387                        .collect::<Vec<_>>()
6388                        .join("::")
6389                };
6390                if fields.is_empty() {
6391                    self.buf.push_str(&variant_name);
6392                } else {
6393                    let _ = write!(self.buf, "{variant_name}(");
6394                    for (i, f) in fields.iter().enumerate() {
6395                        if i > 0 {
6396                            self.buf.push_str(", ");
6397                        }
6398                        self.emit_pattern(f)?;
6399                    }
6400                    self.buf.push(')');
6401                }
6402            }
6403            NodeKind::RecordPat { path, fields, rest } => {
6404                let type_name = if let Some(enum_name) = self.variant_enum_qualifier(path) {
6405                    let variant = path.segments.last().map_or("", |s| s.name.as_str());
6406                    format!("{enum_name}::{variant}")
6407                } else {
6408                    path.segments
6409                        .iter()
6410                        .map(|s| s.name.as_str())
6411                        .collect::<Vec<_>>()
6412                        .join("::")
6413                };
6414                let _ = write!(self.buf, "{type_name} {{ ");
6415                for (i, f) in fields.iter().enumerate() {
6416                    if i > 0 {
6417                        self.buf.push_str(", ");
6418                    }
6419                    let field_name = to_snake_case(&f.name.name);
6420                    if let Some(pat) = &f.pattern {
6421                        let _ = write!(self.buf, "{field_name}: ");
6422                        self.emit_pattern(pat)?;
6423                    } else {
6424                        self.buf.push_str(&field_name);
6425                    }
6426                }
6427                if *rest {
6428                    if !fields.is_empty() {
6429                        self.buf.push_str(", ");
6430                    }
6431                    self.buf.push_str("..");
6432                }
6433                self.buf.push_str(" }");
6434            }
6435            NodeKind::TuplePat { elems } => {
6436                self.buf.push('(');
6437                for (i, e) in elems.iter().enumerate() {
6438                    if i > 0 {
6439                        self.buf.push_str(", ");
6440                    }
6441                    self.emit_pattern(e)?;
6442                }
6443                self.buf.push(')');
6444            }
6445            NodeKind::ListPat { elems, rest } => {
6446                self.buf.push('[');
6447                for (i, e) in elems.iter().enumerate() {
6448                    if i > 0 {
6449                        self.buf.push_str(", ");
6450                    }
6451                    self.emit_pattern(e)?;
6452                }
6453                if let Some(r) = rest {
6454                    if !elems.is_empty() {
6455                        self.buf.push_str(", ");
6456                    }
6457                    self.emit_pattern(r)?;
6458                    self.buf.push_str(" @ ..");
6459                }
6460                self.buf.push(']');
6461            }
6462            NodeKind::OrPat { alternatives } => {
6463                for (i, p) in alternatives.iter().enumerate() {
6464                    if i > 0 {
6465                        self.buf.push_str(" | ");
6466                    }
6467                    self.emit_pattern(p)?;
6468                }
6469            }
6470            NodeKind::GuardPat { pattern, guard } => {
6471                self.emit_pattern(pattern)?;
6472                self.buf.push_str(" if ");
6473                self.emit_expr(guard)?;
6474            }
6475            NodeKind::RangePat { lo, hi, inclusive } => {
6476                self.emit_pattern(lo)?;
6477                if *inclusive {
6478                    self.buf.push_str("..=");
6479                } else {
6480                    self.buf.push_str("..");
6481                }
6482                self.emit_pattern(hi)?;
6483            }
6484            NodeKind::RestPat => {
6485                self.buf.push_str("..");
6486            }
6487            _ => {
6488                self.buf.push('_');
6489            }
6490        }
6491        Ok(())
6492    }
6493
6494    // ── Pipe operator ───────────────────────────────────────────────────────
6495
6496    fn emit_pipe(&mut self, left: &AIRNode, right: &AIRNode) -> Result<(), CodegenError> {
6497        if let NodeKind::Call { callee, args, .. } = &right.kind {
6498            let has_placeholder = args
6499                .iter()
6500                .any(|a| matches!(a.value.kind, NodeKind::Placeholder));
6501            if has_placeholder {
6502                self.emit_expr(callee)?;
6503                self.buf.push('(');
6504                for (i, arg) in args.iter().enumerate() {
6505                    if i > 0 {
6506                        self.buf.push_str(", ");
6507                    }
6508                    if matches!(arg.value.kind, NodeKind::Placeholder) {
6509                        self.emit_expr(left)?;
6510                    } else {
6511                        self.emit_expr(&arg.value)?;
6512                    }
6513                }
6514                self.buf.push(')');
6515                return Ok(());
6516            }
6517        }
6518        // `x |> (|v| …)` pipes into a closure: parenthesize the closure callee
6519        // so the `(left)` call applies to it, not to its body. See
6520        // `emit_callee_rs`.
6521        self.emit_callee_rs(right)?;
6522        self.buf.push('(');
6523        self.emit_expr(left)?;
6524        self.buf.push(')');
6525        Ok(())
6526    }
6527
6528    // ── Helpers ─────────────────────────────────────────────────────────────
6529
6530    fn emit_block_body(&mut self, node: &AIRNode) -> Result<(), CodegenError> {
6531        if let NodeKind::Block { stmts, tail } = &node.kind {
6532            if stmts.is_empty() && tail.is_none() {
6533                // Empty block body.
6534                return Ok(());
6535            }
6536            // Concurrent-pattern detection: names bound in this block whose
6537            // Call RHS should be scheduled via `tokio::spawn` because the
6538            // same name is later `await`ed in the same block. Rust futures
6539            // are lazy, so without spawning, sequential `.await` calls on
6540            // each binding would serialise the work.
6541            let task_bindings = Self::collect_task_bindings(stmts);
6542            let prev = std::mem::replace(&mut self.task_bound_names, task_bindings);
6543            // Seed the move-reuse clone set for this block's `let` bindings: a
6544            // non-`Copy` binding read by value more than once is moved by its
6545            // first by-value consumer, so later free-fn arg passes must clone
6546            // (`E0382`). A binding read even ONCE inside a loop is also reused —
6547            // the loop re-executes that read each iteration, moving it on the
6548            // first pass and leaving it gone on the second (a `let nums = …`
6549            // iterated by a nested `for n in nums` once per outer iteration).
6550            // This mirrors `seed_reused_params`' `count > 1 ||
6551            // identifier_used_in_loop` heuristic. Unioned into (not replacing)
6552            // any outer-block set so a reused binding from an enclosing block
6553            // stays cloned in nested blocks; saved/restored so the additions
6554            // never leak outward.
6555            let prev_reused_let = self.reused_let_bindings.clone();
6556            // Track which `let` bindings hold a Rust collection so an
6557            // interpolation of one formats with `{:?}` (a `Vec`/`HashMap`/
6558            // `HashSet` has no `Display`). See `collection_bindings`.
6559            let prev_collection = self.collection_bindings.clone();
6560            // Track which `let` bindings hold a function/closure (`impl Fn`) so a
6561            // move-reuse of one is *borrowed* (`&f`) rather than `.clone()`d — an
6562            // `impl Fn` opaque type is not `Clone` (E0599). See
6563            // `fn_typed_bindings`.
6564            let prev_fn_typed = self.fn_typed_bindings.clone();
6565            for s in stmts {
6566                if let NodeKind::LetBinding {
6567                    pattern, value, ty, ..
6568                } = &s.kind
6569                {
6570                    if let NodeKind::BindPat { name, .. } = &pattern.kind {
6571                        let rs_name = to_snake_case(&name.name);
6572                        if Self::count_identifier_uses(node, &rs_name) > 1
6573                            || Self::identifier_used_in_loop(node, &rs_name)
6574                        {
6575                            self.reused_let_bindings.insert(rs_name.clone());
6576                        }
6577                        if ty.as_deref().is_some_and(Self::type_is_display_collection)
6578                            || Self::expr_is_collection_valued(value)
6579                        {
6580                            self.collection_bindings.insert(rs_name.clone());
6581                        }
6582                        if ty
6583                            .as_deref()
6584                            .is_some_and(|t| matches!(&t.kind, NodeKind::TypeFunction { .. }))
6585                            || self.rhs_is_fn_valued(value)
6586                        {
6587                            self.fn_typed_bindings.insert(rs_name);
6588                        }
6589                    }
6590                }
6591            }
6592            for s in stmts {
6593                self.emit_node(s)?;
6594            }
6595            // The block's tail expression is in the SAME scope as the block's
6596            // `let` bindings, so it must be emitted while the seeded move-reuse
6597            // clone set is still live — a tail that reuses a block binding by
6598            // value (`println("${join(xs)},${xs.len()}")`, an interpolation tail
6599            // whose first segment moves `xs` and whose second re-reads it) needs
6600            // the same `.clone()` insertion a body statement would get. Restoring
6601            // the block-scope sets BEFORE emitting the tail dropped that seeding
6602            // and re-introduced `E0382`. Restore in every exit path AFTER the
6603            // tail instead.
6604            if let Some(t) = tail {
6605                // A statement tail (`return`/`break`/`continue`/assignment) is
6606                // emitted via the statement emitter — `emit_expr` has no arm
6607                // for these control-flow nodes and would emit
6608                // `/* unsupported */`.
6609                if crate::generator::node_is_statement(t) {
6610                    self.emit_stmt(t)?;
6611                    self.reused_let_bindings = prev_reused_let;
6612                    self.collection_bindings = prev_collection;
6613                    self.fn_typed_bindings = prev_fn_typed;
6614                    self.task_bound_names = prev;
6615                    return Ok(());
6616                }
6617                // Tail expression without semicolon (Rust implicit return).
6618                self.write_indent();
6619                let prev_returning = self.returning_fn_closure;
6620                self.returning_fn_closure = self.return_closure_tail;
6621                let clone_tail = self.tail_ident_needs_clone(t);
6622                let r = self.emit_expr(t);
6623                if clone_tail {
6624                    self.buf.push_str(".clone()");
6625                }
6626                self.returning_fn_closure = prev_returning;
6627                r?;
6628                self.buf.push('\n');
6629            }
6630            self.reused_let_bindings = prev_reused_let;
6631            self.collection_bindings = prev_collection;
6632            self.fn_typed_bindings = prev_fn_typed;
6633            self.task_bound_names = prev;
6634        } else if crate::generator::node_is_statement(node) {
6635            self.emit_stmt(node)?;
6636        } else {
6637            // Single expression as body (implicit return).
6638            self.write_indent();
6639            let prev = self.returning_fn_closure;
6640            self.returning_fn_closure = self.return_closure_tail;
6641            let r = self.emit_expr(node);
6642            self.returning_fn_closure = prev;
6643            r?;
6644            self.buf.push('\n');
6645        }
6646        Ok(())
6647    }
6648
6649    /// Emit a `@test` function body (S7), lowering `expect(...)` assertion
6650    /// chains to Rust `assert!` / `assert_eq!` and falling back to the normal
6651    /// statement emitter for any other statement (`let`, helper calls, …).
6652    fn emit_test_body(&mut self, body: &AIRNode) -> Result<(), CodegenError> {
6653        let emit_one = |this: &mut Self, stmt: &AIRNode| -> Result<(), CodegenError> {
6654            if let Some((assertion, actual, expected)) = crate::generator::classify_assertion(stmt)
6655            {
6656                let a = this.expr_to_string(actual)?;
6657                let line = match assertion {
6658                    crate::generator::TestAssertion::Equal => {
6659                        let e = match expected {
6660                            Some(e) => this.expr_to_string(e)?,
6661                            None => "()".to_string(),
6662                        };
6663                        format!("assert_eq!({a}, {e});")
6664                    }
6665                    crate::generator::TestAssertion::BeTrue => format!("assert!({a});"),
6666                    crate::generator::TestAssertion::BeFalse => format!("assert!(!({a}));"),
6667                    crate::generator::TestAssertion::BeSome => format!("assert!(({a}).is_some());"),
6668                    crate::generator::TestAssertion::BeNone => format!("assert!(({a}).is_none());"),
6669                    crate::generator::TestAssertion::BeOk => format!("assert!(({a}).is_ok());"),
6670                    crate::generator::TestAssertion::BeErr => format!("assert!(({a}).is_err());"),
6671                };
6672                this.writeln(&line);
6673                Ok(())
6674            } else {
6675                this.emit_node(stmt)
6676            }
6677        };
6678        if let NodeKind::Block { stmts, tail } = &body.kind {
6679            for s in stmts {
6680                emit_one(self, s)?;
6681            }
6682            if let Some(t) = tail {
6683                emit_one(self, t)?;
6684            }
6685        } else {
6686            emit_one(self, body)?;
6687        }
6688        Ok(())
6689    }
6690
6691    /// Scan a sequence of block statements and return the set of bound names
6692    /// that are later `await`ed as bare identifiers within the same block.
6693    /// The caller wraps those LetBindings' Call values in `tokio::spawn`.
6694    ///
6695    /// Only direct `let name = call(...)` bindings qualify. Non-call RHS are
6696    /// skipped (nothing to spawn). The binding must be awaited in the same
6697    /// flat block — nested scopes are ignored because we can't prove the
6698    /// binding is still live once control leaves the block.
6699    fn collect_task_bindings(stmts: &[AIRNode]) -> std::collections::HashSet<String> {
6700        let mut awaited: std::collections::HashSet<String> = std::collections::HashSet::new();
6701        for s in stmts {
6702            Self::collect_awaited_identifiers(s, &mut awaited);
6703        }
6704        let mut out = std::collections::HashSet::new();
6705        for s in stmts {
6706            if let NodeKind::LetBinding { pattern, value, .. } = &s.kind {
6707                if let NodeKind::BindPat { name, .. } = &pattern.kind {
6708                    let rs_name = to_snake_case(&name.name);
6709                    if matches!(&value.kind, NodeKind::Call { .. }) && awaited.contains(&rs_name) {
6710                        out.insert(rs_name);
6711                    }
6712                }
6713            }
6714        }
6715        out
6716    }
6717
6718    /// Walk an AIR subtree and record every `await name` where `name` is a
6719    /// bare identifier. Nested function / lambda bodies are not descended —
6720    /// an inner closure awaiting the name doesn't imply the outer block
6721    /// wants a task.
6722    fn collect_awaited_identifiers(node: &AIRNode, out: &mut std::collections::HashSet<String>) {
6723        match &node.kind {
6724            NodeKind::Await { expr } => {
6725                if let NodeKind::Identifier { name } = &expr.kind {
6726                    out.insert(to_snake_case(&name.name));
6727                }
6728                Self::collect_awaited_identifiers(expr, out);
6729            }
6730            NodeKind::Lambda { .. } | NodeKind::FnDecl { .. } => {
6731                // Don't cross function boundaries.
6732            }
6733            NodeKind::Block { stmts, tail } => {
6734                for s in stmts {
6735                    Self::collect_awaited_identifiers(s, out);
6736                }
6737                if let Some(t) = tail {
6738                    Self::collect_awaited_identifiers(t, out);
6739                }
6740            }
6741            NodeKind::LetBinding { value, .. } => {
6742                Self::collect_awaited_identifiers(value, out);
6743            }
6744            NodeKind::Call { callee, args, .. } => {
6745                Self::collect_awaited_identifiers(callee, out);
6746                for a in args {
6747                    Self::collect_awaited_identifiers(&a.value, out);
6748                }
6749            }
6750            NodeKind::MethodCall { receiver, args, .. } => {
6751                Self::collect_awaited_identifiers(receiver, out);
6752                for a in args {
6753                    Self::collect_awaited_identifiers(&a.value, out);
6754                }
6755            }
6756            NodeKind::BinaryOp { left, right, .. } => {
6757                Self::collect_awaited_identifiers(left, out);
6758                Self::collect_awaited_identifiers(right, out);
6759            }
6760            NodeKind::UnaryOp { operand, .. } => {
6761                Self::collect_awaited_identifiers(operand, out);
6762            }
6763            NodeKind::If {
6764                condition,
6765                then_block,
6766                else_block,
6767                ..
6768            } => {
6769                Self::collect_awaited_identifiers(condition, out);
6770                Self::collect_awaited_identifiers(then_block, out);
6771                if let Some(e) = else_block {
6772                    Self::collect_awaited_identifiers(e, out);
6773                }
6774            }
6775            NodeKind::While { condition, body } => {
6776                Self::collect_awaited_identifiers(condition, out);
6777                Self::collect_awaited_identifiers(body, out);
6778            }
6779            NodeKind::For { iterable, body, .. } => {
6780                Self::collect_awaited_identifiers(iterable, out);
6781                Self::collect_awaited_identifiers(body, out);
6782            }
6783            NodeKind::Return { value: Some(v) } | NodeKind::Break { value: Some(v) } => {
6784                Self::collect_awaited_identifiers(v, out);
6785            }
6786            NodeKind::Assign { value, .. } => {
6787                Self::collect_awaited_identifiers(value, out);
6788            }
6789            NodeKind::TupleLiteral { elems } | NodeKind::ListLiteral { elems } => {
6790                for e in elems {
6791                    Self::collect_awaited_identifiers(e, out);
6792                }
6793            }
6794            _ => {}
6795        }
6796    }
6797
6798    fn emit_block_as_expr(&mut self, node: &AIRNode) -> Result<(), CodegenError> {
6799        if let NodeKind::Block { stmts, tail } = &node.kind {
6800            if stmts.is_empty() {
6801                if let Some(t) = tail {
6802                    return self.emit_tail_value(t);
6803                }
6804            }
6805        }
6806        self.emit_expr(node)
6807    }
6808
6809    /// Emit a value-position block tail, cloning a move-reused bare-identifier
6810    /// tail (`{ value }` arm) so a sibling/later use stays live (`E0382`). See
6811    /// [`Self::tail_ident_needs_clone`]; the call-arg and for-iterable emitters
6812    /// apply the same clone in their positions.
6813    fn emit_tail_value(&mut self, tail: &AIRNode) -> Result<(), CodegenError> {
6814        let clone_tail = self.tail_ident_needs_clone(tail);
6815        self.emit_expr(tail)?;
6816        if clone_tail {
6817            self.buf.push_str(".clone()");
6818        }
6819        Ok(())
6820    }
6821
6822    fn pattern_to_binding_name(&self, pat: &AIRNode) -> String {
6823        match &pat.kind {
6824            NodeKind::BindPat { name, .. } => to_snake_case(&name.name),
6825            NodeKind::WildcardPat => "_".into(),
6826            NodeKind::TuplePat { elems } => {
6827                format!(
6828                    "({})",
6829                    elems
6830                        .iter()
6831                        .map(|e| self.pattern_to_binding_name(e))
6832                        .collect::<Vec<_>>()
6833                        .join(", ")
6834                )
6835            }
6836            _ => "_".into(),
6837        }
6838    }
6839
6840    fn pattern_to_rs_binding(&self, pat: &AIRNode) -> String {
6841        self.pattern_to_binding_name(pat)
6842    }
6843
6844    fn type_expr_to_string(&mut self, node: &AIRNode) -> String {
6845        match &node.kind {
6846            NodeKind::TypeNamed { path, args } => {
6847                let name = path
6848                    .segments
6849                    .iter()
6850                    .map(|s| s.name.as_str())
6851                    .collect::<Vec<_>>()
6852                    .join("::");
6853                if args.is_empty() {
6854                    name
6855                } else {
6856                    let arg_strs: Vec<String> = args.iter().map(|a| self.type_to_rs(a)).collect();
6857                    format!("{name}<{}>", arg_strs.join(", "))
6858                }
6859            }
6860            NodeKind::Identifier { name } => name.name.clone(),
6861            _ => "Unknown".into(),
6862        }
6863    }
6864}
6865
6866// ─── Utility functions ───────────────────────────────────────────────────────
6867
6868/// Visibility keyword.
6869fn vis_str(v: Visibility) -> &'static str {
6870    match v {
6871        Visibility::Public => "pub ",
6872        Visibility::Private => "",
6873        Visibility::Internal => "pub(crate) ",
6874    }
6875}
6876
6877/// If `node` is a record construction, return the fully-qualified type path
6878/// used in the constructor. Used by module-level `handle` emission to pick a
6879/// concrete type annotation for the synthesised `const`.
6880fn record_construct_type(node: &AIRNode) -> Option<String> {
6881    if let NodeKind::RecordConstruct { path, .. } = &node.kind {
6882        let joined = path
6883            .segments
6884            .iter()
6885            .map(|s| s.name.as_str())
6886            .collect::<Vec<_>>()
6887            .join("::");
6888        Some(joined)
6889    } else {
6890        None
6891    }
6892}
6893
6894/// Emit a Bock identifier as a Rust identifier — PascalCase names are
6895/// preserved verbatim (they are types, enum variants, or tuple-struct
6896/// constructors), while everything else is converted to snake_case.
6897fn identifier_to_rs(s: &str) -> String {
6898    if s.chars().next().is_some_and(char::is_uppercase) {
6899        s.to_string()
6900    } else {
6901        to_snake_case(s)
6902    }
6903}
6904
6905/// Returns true if `name` is the identifier of a Duration or Instant instance
6906/// method. Used to recognise `d.as_millis()` / `i.elapsed()` calls during codegen.
6907fn is_time_method_name(name: &str) -> bool {
6908    matches!(
6909        name,
6910        "as_nanos"
6911            | "as_millis"
6912            | "as_seconds"
6913            | "is_zero"
6914            | "is_negative"
6915            | "abs"
6916            | "elapsed"
6917            | "duration_since"
6918    )
6919}
6920
6921/// Convert a `PascalCase` or `camelCase` name to `snake_case`.
6922fn to_snake_case(s: &str) -> String {
6923    if s.is_empty() || s == "_" {
6924        return s.to_string();
6925    }
6926    if s.contains('_') && !s.chars().any(|c| c.is_uppercase()) {
6927        return s.to_string();
6928    }
6929    if !s.chars().any(|c| c.is_uppercase()) {
6930        return s.to_string();
6931    }
6932    if s.len() == 1 {
6933        return s.to_lowercase();
6934    }
6935
6936    let mut result = String::with_capacity(s.len() + 4);
6937    let chars: Vec<char> = s.chars().collect();
6938
6939    for (i, &ch) in chars.iter().enumerate() {
6940        if ch.is_uppercase() {
6941            let prev_is_upper = i > 0 && chars[i - 1].is_uppercase();
6942            let prev_is_underscore = i > 0 && chars[i - 1] == '_';
6943            let next_is_lower = i + 1 < chars.len() && chars[i + 1].is_lowercase();
6944            if i > 0 && !prev_is_underscore && (!prev_is_upper || next_is_lower) {
6945                result.push('_');
6946            }
6947            result.push(
6948                ch.to_lowercase()
6949                    .next()
6950                    .expect("lowercase yields at least one char"),
6951            );
6952        } else {
6953            result.push(ch);
6954        }
6955    }
6956    result
6957}
6958
6959/// Convert a name to `UPPER_SNAKE_CASE` for constants.
6960fn to_upper_snake_case(s: &str) -> String {
6961    to_snake_case(s).to_uppercase()
6962}
6963
6964/// Escape special characters in a Rust string literal.
6965fn escape_rs_string(s: &str) -> String {
6966    let mut out = String::with_capacity(s.len());
6967    for ch in s.chars() {
6968        match ch {
6969            '"' => out.push_str("\\\""),
6970            '\\' => out.push_str("\\\\"),
6971            '\n' => out.push_str("\\n"),
6972            '\r' => out.push_str("\\r"),
6973            '\t' => out.push_str("\\t"),
6974            _ => out.push(ch),
6975        }
6976    }
6977    out
6978}
6979
6980/// Escape special characters in a `format!()` format string.
6981fn escape_format_string(s: &str) -> String {
6982    let mut out = String::with_capacity(s.len());
6983    for ch in s.chars() {
6984        match ch {
6985            '"' => out.push_str("\\\""),
6986            '\\' => out.push_str("\\\\"),
6987            '{' => out.push_str("{{"),
6988            '}' => out.push_str("}}"),
6989            _ => out.push(ch),
6990        }
6991    }
6992    out
6993}
6994
6995// ─── Tests ───────────────────────────────────────────────────────────────────
6996
6997#[cfg(test)]
6998mod tests {
6999    use super::*;
7000    use bock_air::{AirArg, AirMapEntry, AirRecordField};
7001    use bock_ast::{
7002        GenericParam, Ident, ImportItems, ImportedName, ModulePath, RecordDeclField, TypePath,
7003    };
7004    use bock_errors::{FileId, Span};
7005
7006    fn span() -> Span {
7007        Span {
7008            file: FileId(0),
7009            start: 0,
7010            end: 0,
7011        }
7012    }
7013
7014    fn ident(name: &str) -> Ident {
7015        Ident {
7016            name: name.to_string(),
7017            span: span(),
7018        }
7019    }
7020
7021    fn type_path(segments: &[&str]) -> TypePath {
7022        TypePath {
7023            segments: segments.iter().map(|s| ident(s)).collect(),
7024            span: span(),
7025        }
7026    }
7027
7028    fn mod_path(segments: &[&str]) -> ModulePath {
7029        ModulePath {
7030            segments: segments.iter().map(|s| ident(s)).collect(),
7031            span: span(),
7032        }
7033    }
7034
7035    fn imported_name(name: &str) -> ImportedName {
7036        ImportedName {
7037            span: span(),
7038            name: ident(name),
7039            alias: None,
7040        }
7041    }
7042
7043    fn record_field(name: &str, ty_name: &str) -> RecordDeclField {
7044        RecordDeclField {
7045            id: 0,
7046            span: span(),
7047            name: ident(name),
7048            ty: TypeExpr::Named {
7049                id: 0,
7050                path: type_path(&[ty_name]),
7051                args: vec![],
7052                span: span(),
7053            },
7054            default: None,
7055        }
7056    }
7057
7058    fn node(id: u32, kind: NodeKind) -> AIRNode {
7059        AIRNode::new(id, span(), kind)
7060    }
7061
7062    fn int_lit(id: u32, val: &str) -> AIRNode {
7063        node(
7064            id,
7065            NodeKind::Literal {
7066                lit: Literal::Int(val.into()),
7067            },
7068        )
7069    }
7070
7071    fn str_lit(id: u32, val: &str) -> AIRNode {
7072        node(
7073            id,
7074            NodeKind::Literal {
7075                lit: Literal::String(val.into()),
7076            },
7077        )
7078    }
7079
7080    fn bool_lit(id: u32, val: bool) -> AIRNode {
7081        node(
7082            id,
7083            NodeKind::Literal {
7084                lit: Literal::Bool(val),
7085            },
7086        )
7087    }
7088
7089    fn id_node(id: u32, name: &str) -> AIRNode {
7090        node(id, NodeKind::Identifier { name: ident(name) })
7091    }
7092
7093    fn bind_pat(id: u32, name: &str) -> AIRNode {
7094        node(
7095            id,
7096            NodeKind::BindPat {
7097                name: ident(name),
7098                is_mut: false,
7099            },
7100        )
7101    }
7102
7103    fn typed_param_node(id: u32, name: &str, ty_name: &str) -> AIRNode {
7104        node(
7105            id,
7106            NodeKind::Param {
7107                pattern: Box::new(bind_pat(id + 100, name)),
7108                ty: Some(Box::new(node(
7109                    id + 200,
7110                    NodeKind::TypeNamed {
7111                        path: type_path(&[ty_name]),
7112                        args: vec![],
7113                    },
7114                ))),
7115                default: None,
7116            },
7117        )
7118    }
7119
7120    fn block(id: u32, stmts: Vec<AIRNode>, tail: Option<AIRNode>) -> AIRNode {
7121        node(
7122            id,
7123            NodeKind::Block {
7124                stmts,
7125                tail: tail.map(Box::new),
7126            },
7127        )
7128    }
7129
7130    fn module(imports: Vec<AIRNode>, items: Vec<AIRNode>) -> AIRNode {
7131        node(
7132            0,
7133            NodeKind::Module {
7134                path: None,
7135                annotations: vec![],
7136                imports,
7137                items,
7138            },
7139        )
7140    }
7141
7142    fn gen(module: &AIRNode) -> String {
7143        let gen = RsGenerator::new();
7144        let result = gen.generate_module(module).unwrap();
7145        result.files[0].content.clone()
7146    }
7147
7148    /// Run `rustc --edition 2021 --crate-type lib` to validate syntax.
7149    fn check_rs_syntax(code: &str) -> bool {
7150        use std::io::Write;
7151        use std::process::Command;
7152        let id = std::thread::current().id();
7153        let dir = std::env::temp_dir().join(format!("bock_rs_test_{id:?}"));
7154        let _ = std::fs::create_dir_all(&dir);
7155        let path = dir.join("test_output.rs");
7156        {
7157            let mut f = std::fs::File::create(&path).unwrap();
7158            f.write_all(code.as_bytes()).unwrap();
7159        }
7160        let output = Command::new("rustc")
7161            .args([
7162                "--edition",
7163                "2021",
7164                "--crate-type",
7165                "lib",
7166                "-o",
7167                dir.join("test_output.rlib").to_str().unwrap(),
7168            ])
7169            .arg(&path)
7170            .stderr(std::process::Stdio::piped())
7171            .output();
7172        match output {
7173            Ok(o) => {
7174                if !o.status.success() {
7175                    eprintln!("rustc stderr: {}", String::from_utf8_lossy(&o.stderr));
7176                }
7177                o.status.success()
7178            }
7179            Err(_) => false,
7180        }
7181    }
7182
7183    // ── Basic tests ─────────────────────────────────────────────────────────
7184
7185    #[test]
7186    fn implements_code_generator_trait() {
7187        let gen = RsGenerator::new();
7188        assert_eq!(gen.target().id, "rust");
7189    }
7190
7191    #[test]
7192    fn empty_module() {
7193        let m = module(vec![], vec![]);
7194        let out = gen(&m);
7195        assert_eq!(out, "");
7196    }
7197
7198    #[test]
7199    fn simple_function() {
7200        let body = block(2, vec![], Some(int_lit(3, "42")));
7201        let f = node(
7202            1,
7203            NodeKind::FnDecl {
7204                annotations: vec![],
7205                visibility: Visibility::Private,
7206                is_async: false,
7207                name: ident("answer"),
7208                generic_params: vec![],
7209                params: vec![],
7210                return_type: None,
7211                effect_clause: vec![],
7212                where_clause: vec![],
7213                body: Box::new(body),
7214            },
7215        );
7216        let out = gen(&module(vec![], vec![f]));
7217        assert!(out.contains("fn answer()"), "got: {out}");
7218        assert!(out.contains("42"), "got: {out}");
7219    }
7220
7221    #[test]
7222    fn public_function_with_params() {
7223        let body = block(
7224            5,
7225            vec![],
7226            Some(node(
7227                6,
7228                NodeKind::BinaryOp {
7229                    op: BinOp::Add,
7230                    left: Box::new(id_node(7, "a")),
7231                    right: Box::new(id_node(8, "b")),
7232                },
7233            )),
7234        );
7235        let f = node(
7236            1,
7237            NodeKind::FnDecl {
7238                annotations: vec![],
7239                visibility: Visibility::Public,
7240                is_async: false,
7241                name: ident("add"),
7242                generic_params: vec![],
7243                params: vec![
7244                    typed_param_node(2, "a", "Int"),
7245                    typed_param_node(3, "b", "Int"),
7246                ],
7247                return_type: Some(Box::new(node(
7248                    4,
7249                    NodeKind::TypeNamed {
7250                        path: type_path(&["Int"]),
7251                        args: vec![],
7252                    },
7253                ))),
7254                effect_clause: vec![],
7255                where_clause: vec![],
7256                body: Box::new(body),
7257            },
7258        );
7259        let out = gen(&module(vec![], vec![f]));
7260        assert!(
7261            out.contains("pub fn add(a: i64, b: i64) -> i64"),
7262            "got: {out}"
7263        );
7264        assert!(out.contains("(a + b)"), "got: {out}");
7265    }
7266
7267    #[test]
7268    fn record_to_struct() {
7269        let record = node(
7270            1,
7271            NodeKind::RecordDecl {
7272                annotations: vec![],
7273                visibility: Visibility::Public,
7274                name: ident("Point"),
7275                generic_params: vec![],
7276                fields: vec![record_field("x", "Float"), record_field("y", "Float")],
7277            },
7278        );
7279        let out = gen(&module(vec![], vec![record]));
7280        assert!(out.contains("pub struct Point {"), "got: {out}");
7281        assert!(out.contains("pub x: f64,"), "got: {out}");
7282        assert!(out.contains("pub y: f64,"), "got: {out}");
7283    }
7284
7285    #[test]
7286    fn enum_with_variants() {
7287        let e = node(
7288            1,
7289            NodeKind::EnumDecl {
7290                annotations: vec![],
7291                visibility: Visibility::Public,
7292                name: ident("Color"),
7293                generic_params: vec![],
7294                variants: vec![
7295                    node(
7296                        2,
7297                        NodeKind::EnumVariant {
7298                            name: ident("Red"),
7299                            payload: EnumVariantPayload::Unit,
7300                        },
7301                    ),
7302                    node(
7303                        3,
7304                        NodeKind::EnumVariant {
7305                            name: ident("Green"),
7306                            payload: EnumVariantPayload::Unit,
7307                        },
7308                    ),
7309                    node(
7310                        4,
7311                        NodeKind::EnumVariant {
7312                            name: ident("Rgb"),
7313                            payload: EnumVariantPayload::Struct(vec![
7314                                record_field("r", "Int"),
7315                                record_field("g", "Int"),
7316                            ]),
7317                        },
7318                    ),
7319                    node(
7320                        7,
7321                        NodeKind::EnumVariant {
7322                            name: ident("Custom"),
7323                            payload: EnumVariantPayload::Tuple(vec![node(
7324                                8,
7325                                NodeKind::TypeNamed {
7326                                    path: type_path(&["String"]),
7327                                    args: vec![],
7328                                },
7329                            )]),
7330                        },
7331                    ),
7332                ],
7333            },
7334        );
7335        let out = gen(&module(vec![], vec![e]));
7336        assert!(out.contains("pub enum Color {"), "got: {out}");
7337        assert!(out.contains("Red,"), "got: {out}");
7338        assert!(out.contains("Green,"), "got: {out}");
7339        assert!(out.contains("Rgb {"), "got: {out}");
7340        assert!(out.contains("r: i64,"), "got: {out}");
7341        assert!(out.contains("Custom(String),"), "got: {out}");
7342    }
7343
7344    #[test]
7345    fn trait_declaration() {
7346        let t = node(
7347            1,
7348            NodeKind::TraitDecl {
7349                annotations: vec![],
7350                visibility: Visibility::Public,
7351                is_platform: false,
7352                name: ident("Printable"),
7353                generic_params: vec![],
7354                associated_types: vec![],
7355                methods: vec![node(
7356                    2,
7357                    NodeKind::FnDecl {
7358                        annotations: vec![],
7359                        visibility: Visibility::Public,
7360                        is_async: false,
7361                        name: ident("print"),
7362                        generic_params: vec![],
7363                        // An *instance* trait method leads with an explicit `self`
7364                        // param (as real lowering produces) and emits `&self`. A
7365                        // method with no `self` is an *associated* function (e.g.
7366                        // `From::from`) and emits with no receiver — see
7367                        // `trait_declaration_associated_fn`.
7368                        params: vec![self_param(4)],
7369                        return_type: None,
7370                        effect_clause: vec![],
7371                        where_clause: vec![],
7372                        body: Box::new(block(3, vec![], None)),
7373                    },
7374                )],
7375            },
7376        );
7377        let out = gen(&module(vec![], vec![t]));
7378        assert!(out.contains("pub trait Printable {"), "got: {out}");
7379        assert!(out.contains("fn print(&self);"), "got: {out}");
7380    }
7381
7382    /// Q-prim-assoc: an *associated function* trait method (no `self` receiver,
7383    /// e.g. `From::from(value: T) -> Self`) must be declared with NO receiver and
7384    /// — when it returns a `Self`-bearing type by value — a `where Self: Sized`
7385    /// bound. The previous codegen injected a spurious `&self`, which made
7386    /// `core.convert`'s `From`/`TryFrom` traits uncompilable on Rust (`E0186`).
7387    #[test]
7388    fn trait_declaration_associated_fn() {
7389        let t = node(
7390            1,
7391            NodeKind::TraitDecl {
7392                annotations: vec![],
7393                visibility: Visibility::Public,
7394                is_platform: false,
7395                name: ident("MakeSelf"),
7396                generic_params: vec![],
7397                associated_types: vec![],
7398                methods: vec![node(
7399                    2,
7400                    NodeKind::FnDecl {
7401                        annotations: vec![],
7402                        visibility: Visibility::Public,
7403                        is_async: false,
7404                        name: ident("make"),
7405                        generic_params: vec![],
7406                        // No `self` param → associated function.
7407                        params: vec![],
7408                        return_type: Some(Box::new(node(3, NodeKind::TypeSelf))),
7409                        effect_clause: vec![],
7410                        where_clause: vec![],
7411                        body: Box::new(block(5, vec![], None)),
7412                    },
7413                )],
7414            },
7415        );
7416        let out = gen(&module(vec![], vec![t]));
7417        assert!(
7418            out.contains("fn make() -> Self where Self: Sized;"),
7419            "got: {out}"
7420        );
7421        assert!(!out.contains("&self"), "must not inject a receiver: {out}");
7422    }
7423
7424    #[test]
7425    fn impl_block() {
7426        let imp = node(
7427            1,
7428            NodeKind::ImplBlock {
7429                annotations: vec![],
7430                generic_params: vec![],
7431                trait_path: Some(type_path(&["Printable"])),
7432                trait_args: vec![],
7433                target: Box::new(node(
7434                    2,
7435                    NodeKind::TypeNamed {
7436                        path: type_path(&["Point"]),
7437                        args: vec![],
7438                    },
7439                )),
7440                where_clause: vec![],
7441                methods: vec![node(
7442                    3,
7443                    NodeKind::FnDecl {
7444                        annotations: vec![],
7445                        visibility: Visibility::Public,
7446                        is_async: false,
7447                        name: ident("print"),
7448                        generic_params: vec![],
7449                        // An instance method leads with `self` (as real lowering
7450                        // produces); a method with no `self` is an *associated*
7451                        // function and emits with no receiver.
7452                        params: vec![self_param(6)],
7453                        return_type: None,
7454                        effect_clause: vec![],
7455                        where_clause: vec![],
7456                        body: Box::new(block(4, vec![], Some(str_lit(5, "point")))),
7457                    },
7458                )],
7459            },
7460        );
7461        let out = gen(&module(vec![], vec![imp]));
7462        assert!(out.contains("impl Printable for Point {"), "got: {out}");
7463        assert!(out.contains("fn print(&self)"), "got: {out}");
7464    }
7465
7466    fn self_param(id: u32) -> AIRNode {
7467        node(
7468            id,
7469            NodeKind::Param {
7470                pattern: Box::new(bind_pat(id + 100, "self")),
7471                ty: None,
7472                default: None,
7473            },
7474        )
7475    }
7476
7477    /// A method whose declared params lead with `self` must emit a native
7478    /// `&self` receiver — not both `&self` and a stray `self: _` param
7479    /// (codegen-correctness defect 3).
7480    #[test]
7481    fn self_method_consumes_self_param() {
7482        let field = node(
7483            10,
7484            NodeKind::FieldAccess {
7485                object: Box::new(id_node(11, "self")),
7486                field: ident("x"),
7487            },
7488        );
7489        let imp = node(
7490            1,
7491            NodeKind::ImplBlock {
7492                annotations: vec![],
7493                generic_params: vec![],
7494                trait_path: None,
7495                trait_args: vec![],
7496                target: Box::new(node(
7497                    2,
7498                    NodeKind::TypeNamed {
7499                        path: type_path(&["Point"]),
7500                        args: vec![],
7501                    },
7502                )),
7503                where_clause: vec![],
7504                methods: vec![node(
7505                    3,
7506                    NodeKind::FnDecl {
7507                        annotations: vec![],
7508                        visibility: Visibility::Public,
7509                        is_async: false,
7510                        name: ident("get_x"),
7511                        generic_params: vec![],
7512                        params: vec![self_param(4)],
7513                        return_type: None,
7514                        effect_clause: vec![],
7515                        where_clause: vec![],
7516                        body: Box::new(block(5, vec![], Some(field))),
7517                    },
7518                )],
7519            },
7520        );
7521        let out = gen(&module(vec![], vec![imp]));
7522        assert!(out.contains("fn get_x(&self)"), "got: {out}");
7523        assert!(
7524            !out.contains("self: _"),
7525            "self param leaked as a positional param: {out}"
7526        );
7527    }
7528
7529    /// A desugared instance call `Call(FieldAccess(p, m), [p, x])` emits
7530    /// `p.m(x)` — the prepended self arg is dropped (defect 3, call site).
7531    #[test]
7532    fn self_method_call_drops_prepended_self() {
7533        let recv = id_node(20, "p");
7534        let callee = node(
7535            21,
7536            NodeKind::FieldAccess {
7537                object: Box::new(recv.clone()),
7538                field: ident("scale"),
7539            },
7540        );
7541        let call = node(
7542            22,
7543            NodeKind::Call {
7544                callee: Box::new(callee),
7545                // First arg shares the receiver\'s NodeId (id 20) — the marker
7546                // the lowerer sets for a desugared method call.
7547                args: vec![
7548                    AirArg {
7549                        label: None,
7550                        value: recv,
7551                    },
7552                    AirArg {
7553                        label: None,
7554                        value: int_lit(23, "4"),
7555                    },
7556                ],
7557                type_args: vec![],
7558            },
7559        );
7560        let mut ctx = RsEmitCtx::new();
7561        ctx.emit_expr(&call).unwrap();
7562        assert_eq!(ctx.buf, "p.scale(4_i64)", "got: {}", ctx.buf);
7563    }
7564
7565    /// Build a desugared `recv.method(extra)` call carrying the checker's
7566    /// `recv_kind` annotation, as the primitive-bridge consumer sees it.
7567    fn annotated_bridge_call(method: &str, tag: &str, extra: Vec<AIRNode>) -> AIRNode {
7568        let recv = int_lit(20, "1");
7569        let callee = node(
7570            21,
7571            NodeKind::FieldAccess {
7572                object: Box::new(recv.clone()),
7573                field: ident(method),
7574            },
7575        );
7576        let mut args = vec![AirArg {
7577            label: None,
7578            value: recv,
7579        }];
7580        args.extend(extra.into_iter().map(|value| AirArg { label: None, value }));
7581        let mut call = node(
7582            22,
7583            NodeKind::Call {
7584                callee: Box::new(callee),
7585                args,
7586                type_args: vec![],
7587            },
7588        );
7589        call.metadata.insert(
7590            bock_types::checker::RECV_KIND_META_KEY.to_string(),
7591            bock_air::Value::String(tag.to_string()),
7592        );
7593        call
7594    }
7595
7596    /// The Rust backend consumes the `recv_kind` annotation: `(1).compare(2)` on
7597    /// an `Int` lowers to `i64::cmp` (not the failing `1_i64.compare(2_i64)`).
7598    #[test]
7599    fn primitive_bridge_compare_int_emits_cmp() {
7600        let call = annotated_bridge_call("compare", "Primitive:Int", vec![int_lit(23, "2")]);
7601        let mut ctx = RsEmitCtx::new();
7602        ctx.emit_expr(&call).unwrap();
7603        assert_eq!(ctx.buf, "(1_i64).cmp(&(2_i64))", "got: {}", ctx.buf);
7604    }
7605
7606    /// A float `compare` uses `partial_cmp(...).unwrap()` (floats are `PartialOrd`).
7607    #[test]
7608    fn primitive_bridge_compare_float_uses_partial_cmp() {
7609        let recv = node(
7610            20,
7611            NodeKind::Literal {
7612                lit: Literal::Float("1.0".into()),
7613            },
7614        );
7615        let callee = node(
7616            21,
7617            NodeKind::FieldAccess {
7618                object: Box::new(recv.clone()),
7619                field: ident("compare"),
7620            },
7621        );
7622        let mut call = node(
7623            22,
7624            NodeKind::Call {
7625                callee: Box::new(callee),
7626                args: vec![
7627                    AirArg {
7628                        label: None,
7629                        value: recv,
7630                    },
7631                    AirArg {
7632                        label: None,
7633                        value: node(
7634                            23,
7635                            NodeKind::Literal {
7636                                lit: Literal::Float("2.0".into()),
7637                            },
7638                        ),
7639                    },
7640                ],
7641                type_args: vec![],
7642            },
7643        );
7644        call.metadata.insert(
7645            bock_types::checker::RECV_KIND_META_KEY.to_string(),
7646            bock_air::Value::String("Primitive:Float".to_string()),
7647        );
7648        let mut ctx = RsEmitCtx::new();
7649        ctx.emit_expr(&call).unwrap();
7650        assert_eq!(
7651            ctx.buf, "(1.0_f64).partial_cmp(&(2.0_f64)).unwrap()",
7652            "got: {}",
7653            ctx.buf
7654        );
7655    }
7656
7657    /// `eq` lowers to `==`; `to_string` to `.to_string()`.
7658    #[test]
7659    fn primitive_bridge_eq_and_to_string() {
7660        let eq_call = annotated_bridge_call("eq", "Primitive:Int", vec![int_lit(23, "2")]);
7661        let mut ctx = RsEmitCtx::new();
7662        ctx.emit_expr(&eq_call).unwrap();
7663        assert_eq!(ctx.buf, "((1_i64) == (2_i64))", "got: {}", ctx.buf);
7664
7665        let ts_call = annotated_bridge_call("to_string", "Primitive:Int", vec![]);
7666        let mut ctx = RsEmitCtx::new();
7667        ctx.emit_expr(&ts_call).unwrap();
7668        assert_eq!(ctx.buf, "(1_i64).to_string()", "got: {}", ctx.buf);
7669    }
7670
7671    /// Q-rust-equatable-eq-collision: a `.eq` desugared self-call whose receiver
7672    /// is a user type with an explicit `impl Equatable` (so it also carries the
7673    /// DQ31 delegating `PartialEq`) is emitted as the fully-qualified
7674    /// `Equatable::eq(&a, &b)`, not the ambiguous `a.eq(&b)` (E0034).
7675    #[test]
7676    fn user_equatable_eq_emits_fully_qualified_trait_call() {
7677        // `a.eq(b)` desugared: Call(FieldAccess(a, eq), [a, b]), stamped with
7678        // the checker's `recv_kind = "User:Key"`.
7679        let recv = id_node(20, "a");
7680        let other = id_node(23, "b");
7681        let callee = node(
7682            21,
7683            NodeKind::FieldAccess {
7684                object: Box::new(recv.clone()),
7685                field: ident("eq"),
7686            },
7687        );
7688        let mut call = node(
7689            22,
7690            NodeKind::Call {
7691                callee: Box::new(callee),
7692                args: vec![
7693                    AirArg {
7694                        label: None,
7695                        value: recv,
7696                    },
7697                    AirArg {
7698                        label: None,
7699                        value: other,
7700                    },
7701                ],
7702                type_args: vec![],
7703            },
7704        );
7705        call.metadata.insert(
7706            bock_types::checker::RECV_KIND_META_KEY.to_string(),
7707            bock_air::Value::String("User:Key".to_string()),
7708        );
7709
7710        let mut ctx = RsEmitCtx::new();
7711        ctx.user_equatable_types.insert("Key".to_string());
7712        ctx.emit_expr(&call).unwrap();
7713        assert_eq!(ctx.buf, "Equatable::eq(&a, &b)", "got: {}", ctx.buf);
7714    }
7715
7716    /// The disambiguation is gated on the receiver being a *registered*
7717    /// explicit-`impl Equatable` user type. A same-named `eq` on a type with no
7718    /// such impl (not in the registry → no delegating `PartialEq`, no ambiguity)
7719    /// keeps the plain value-receiver method form.
7720    #[test]
7721    fn non_equatable_eq_keeps_plain_method_call() {
7722        let recv = id_node(20, "a");
7723        let other = id_node(23, "b");
7724        let callee = node(
7725            21,
7726            NodeKind::FieldAccess {
7727                object: Box::new(recv.clone()),
7728                field: ident("eq"),
7729            },
7730        );
7731        let mut call = node(
7732            22,
7733            NodeKind::Call {
7734                callee: Box::new(callee),
7735                args: vec![
7736                    AirArg {
7737                        label: None,
7738                        value: recv,
7739                    },
7740                    AirArg {
7741                        label: None,
7742                        value: other,
7743                    },
7744                ],
7745                type_args: vec![],
7746            },
7747        );
7748        call.metadata.insert(
7749            bock_types::checker::RECV_KIND_META_KEY.to_string(),
7750            bock_air::Value::String("User:Other".to_string()),
7751        );
7752
7753        // `Other` is NOT registered as an explicit-`impl Equatable` type.
7754        let mut ctx = RsEmitCtx::new();
7755        ctx.emit_expr(&call).unwrap();
7756        assert!(
7757            ctx.buf.contains("a.eq(") && !ctx.buf.contains("Equatable::eq"),
7758            "non-Equatable eq should stay a plain method call, got: {}",
7759            ctx.buf
7760        );
7761    }
7762
7763    /// Without the annotation, the call falls through to the generic
7764    /// desugared-self-call lowering (no bridge) — so the annotation is what
7765    /// drives the bridge.
7766    #[test]
7767    fn no_annotation_no_bridge() {
7768        let recv = int_lit(20, "1");
7769        let callee = node(
7770            21,
7771            NodeKind::FieldAccess {
7772                object: Box::new(recv.clone()),
7773                field: ident("compare"),
7774            },
7775        );
7776        let call = node(
7777            22,
7778            NodeKind::Call {
7779                callee: Box::new(callee),
7780                args: vec![
7781                    AirArg {
7782                        label: None,
7783                        value: recv,
7784                    },
7785                    AirArg {
7786                        label: None,
7787                        value: int_lit(23, "2"),
7788                    },
7789                ],
7790                type_args: vec![],
7791            },
7792        );
7793        let mut ctx = RsEmitCtx::new();
7794        ctx.emit_expr(&call).unwrap();
7795        // Generic desugared-self path: `recv.compare(rest)`.
7796        assert_eq!(ctx.buf, "1_i64.compare(2_i64)", "got: {}", ctx.buf);
7797    }
7798
7799    /// Prelude `Ordering` variants lower to Rust's native `std::cmp::Ordering`,
7800    /// self-contained without the `core.compare` enum decl.
7801    #[test]
7802    fn ordering_variant_emits_std_cmp_ordering() {
7803        let mut ctx = RsEmitCtx::new();
7804        ctx.emit_expr(&id_node(1, "Less")).unwrap();
7805        assert_eq!(ctx.buf, "std::cmp::Ordering::Less", "got: {}", ctx.buf);
7806    }
7807
7808    #[test]
7809    fn effect_as_trait() {
7810        let eff = node(
7811            1,
7812            NodeKind::EffectDecl {
7813                annotations: vec![],
7814                visibility: Visibility::Public,
7815                name: ident("Log"),
7816                generic_params: vec![],
7817                components: vec![],
7818                operations: vec![node(
7819                    2,
7820                    NodeKind::FnDecl {
7821                        annotations: vec![],
7822                        visibility: Visibility::Public,
7823                        is_async: false,
7824                        name: ident("log"),
7825                        generic_params: vec![],
7826                        params: vec![typed_param_node(3, "msg", "String")],
7827                        return_type: None,
7828                        effect_clause: vec![],
7829                        where_clause: vec![],
7830                        body: Box::new(block(4, vec![], None)),
7831                    },
7832                )],
7833            },
7834        );
7835        let out = gen(&module(vec![], vec![eff]));
7836        assert!(out.contains("pub trait Log {"), "got: {out}");
7837        assert!(out.contains("fn log(&self, msg: String)"), "got: {out}");
7838    }
7839
7840    #[test]
7841    fn function_with_effects() {
7842        let body = block(3, vec![], Some(int_lit(4, "0")));
7843        let f = node(
7844            1,
7845            NodeKind::FnDecl {
7846                annotations: vec![],
7847                visibility: Visibility::Public,
7848                is_async: false,
7849                name: ident("process"),
7850                generic_params: vec![],
7851                params: vec![typed_param_node(2, "data", "String")],
7852                return_type: Some(Box::new(node(
7853                    5,
7854                    NodeKind::TypeNamed {
7855                        path: type_path(&["Int"]),
7856                        args: vec![],
7857                    },
7858                ))),
7859                effect_clause: vec![type_path(&["Log"]), type_path(&["Clock"])],
7860                where_clause: vec![],
7861                body: Box::new(body),
7862            },
7863        );
7864        let out = gen(&module(vec![], vec![f]));
7865        assert!(
7866            out.contains("pub fn process(data: String, log: &impl Log, clock: &impl Clock) -> i64"),
7867            "got: {out}"
7868        );
7869    }
7870
7871    /// Q-clock-handler-routing: inside a `with Clock` function the §18.3.1 time
7872    /// builtins route through the in-scope `clock` handler — `Instant.now()` →
7873    /// `clock.now_monotonic()`, `sleep(d)` → `clock.sleep(d)`, and the derived
7874    /// `start.elapsed()` via `clock.now_monotonic().duration_since(start)` — NOT
7875    /// the inlined host primitives (`std::time::Instant::now()` /
7876    /// `tokio::time::sleep`).
7877    #[test]
7878    fn clock_time_ops_route_through_handler() {
7879        let out = gen(&module(vec![], vec![clock_timed_fn()]));
7880        assert!(out.contains("clock.now_monotonic()"), "got: {out}");
7881        assert!(out.contains("clock.sleep("), "got: {out}");
7882        assert!(
7883            !out.contains("std::time::Instant::now()"),
7884            "host clock primitive leaked past the handler: {out}"
7885        );
7886        assert!(
7887            !out.contains("tokio::time::sleep"),
7888            "host sleep primitive leaked past the handler: {out}"
7889        );
7890    }
7891
7892    /// `Duration` / `Instant` used as type annotations must render their Rust
7893    /// value representations (`i64` / `std::time::Instant`), not the undefined
7894    /// identifiers, so a `Clock` handler impl compiles (Q-clock-handler-routing
7895    /// supporting fix).
7896    #[test]
7897    fn builtin_time_types_map_to_rust() {
7898        let f = node(
7899            1,
7900            NodeKind::FnDecl {
7901                annotations: vec![],
7902                visibility: Visibility::Public,
7903                is_async: false,
7904                name: ident("span"),
7905                generic_params: vec![],
7906                params: vec![typed_param_node(2, "d", "Duration")],
7907                return_type: Some(Box::new(node(
7908                    3,
7909                    NodeKind::TypeNamed {
7910                        path: type_path(&["Instant"]),
7911                        args: vec![],
7912                    },
7913                ))),
7914                effect_clause: vec![],
7915                where_clause: vec![],
7916                body: Box::new(block(10, vec![], None)),
7917            },
7918        );
7919        let out = gen(&module(vec![], vec![f]));
7920        assert!(out.contains("d: i64"), "Duration annotation: {out}");
7921        assert!(
7922            out.contains("-> std::time::Instant"),
7923            "Instant annotation: {out}"
7924        );
7925    }
7926
7927    /// Builds `fn timed() with Clock { let start = Instant.now(); sleep(
7928    /// Duration.millis(1)); let d = start.elapsed() }` — the `with Clock` clause
7929    /// puts the `clock` handler in scope so the time builtins route through it.
7930    fn clock_timed_fn() -> AIRNode {
7931        let instant_now = node(
7932            40,
7933            NodeKind::Call {
7934                callee: Box::new(node(
7935                    41,
7936                    NodeKind::FieldAccess {
7937                        object: Box::new(id_node(42, "Instant")),
7938                        field: ident("now"),
7939                    },
7940                )),
7941                args: vec![],
7942                type_args: vec![],
7943            },
7944        );
7945        let duration_millis = node(
7946            50,
7947            NodeKind::Call {
7948                callee: Box::new(node(
7949                    51,
7950                    NodeKind::FieldAccess {
7951                        object: Box::new(id_node(52, "Duration")),
7952                        field: ident("millis"),
7953                    },
7954                )),
7955                args: vec![AirArg {
7956                    label: None,
7957                    value: int_lit(53, "1"),
7958                }],
7959                type_args: vec![],
7960            },
7961        );
7962        let sleep_call = node(
7963            60,
7964            NodeKind::Call {
7965                callee: Box::new(id_node(61, "sleep")),
7966                args: vec![AirArg {
7967                    label: None,
7968                    value: duration_millis,
7969                }],
7970                type_args: vec![],
7971            },
7972        );
7973        let elapsed_call = node(
7974            70,
7975            NodeKind::MethodCall {
7976                receiver: Box::new(id_node(71, "start")),
7977                method: ident("elapsed"),
7978                type_args: vec![],
7979                args: vec![],
7980            },
7981        );
7982        let body = block(
7983            30,
7984            vec![
7985                node(
7986                    31,
7987                    NodeKind::LetBinding {
7988                        is_mut: false,
7989                        pattern: Box::new(bind_pat(32, "start")),
7990                        ty: None,
7991                        value: Box::new(instant_now),
7992                    },
7993                ),
7994                sleep_call,
7995                node(
7996                    33,
7997                    NodeKind::LetBinding {
7998                        is_mut: false,
7999                        pattern: Box::new(bind_pat(34, "d")),
8000                        ty: None,
8001                        value: Box::new(elapsed_call),
8002                    },
8003                ),
8004            ],
8005            None,
8006        );
8007        node(
8008            1,
8009            NodeKind::FnDecl {
8010                annotations: vec![],
8011                visibility: Visibility::Private,
8012                is_async: false,
8013                name: ident("timed"),
8014                generic_params: vec![],
8015                params: vec![],
8016                return_type: None,
8017                effect_clause: vec![type_path(&["Clock"])],
8018                where_clause: vec![],
8019                body: Box::new(body),
8020            },
8021        )
8022    }
8023
8024    #[test]
8025    fn ownership_borrow() {
8026        let borrow = node(
8027            1,
8028            NodeKind::Borrow {
8029                expr: Box::new(id_node(2, "x")),
8030            },
8031        );
8032        let m = module(
8033            vec![],
8034            vec![node(
8035                3,
8036                NodeKind::FnDecl {
8037                    annotations: vec![],
8038                    visibility: Visibility::Private,
8039                    is_async: false,
8040                    name: ident("test"),
8041                    generic_params: vec![],
8042                    params: vec![],
8043                    return_type: None,
8044                    effect_clause: vec![],
8045                    where_clause: vec![],
8046                    body: Box::new(block(4, vec![], Some(borrow))),
8047                },
8048            )],
8049        );
8050        let out = gen(&m);
8051        assert!(out.contains("&x"), "got: {out}");
8052    }
8053
8054    #[test]
8055    fn ownership_mutable_borrow() {
8056        let mborrow = node(
8057            1,
8058            NodeKind::MutableBorrow {
8059                expr: Box::new(id_node(2, "x")),
8060            },
8061        );
8062        let m = module(
8063            vec![],
8064            vec![node(
8065                3,
8066                NodeKind::FnDecl {
8067                    annotations: vec![],
8068                    visibility: Visibility::Private,
8069                    is_async: false,
8070                    name: ident("test"),
8071                    generic_params: vec![],
8072                    params: vec![],
8073                    return_type: None,
8074                    effect_clause: vec![],
8075                    where_clause: vec![],
8076                    body: Box::new(block(4, vec![], Some(mborrow))),
8077                },
8078            )],
8079        );
8080        let out = gen(&m);
8081        assert!(out.contains("&mut x"), "got: {out}");
8082    }
8083
8084    #[test]
8085    fn let_binding_with_mut() {
8086        let let_node = node(
8087            1,
8088            NodeKind::LetBinding {
8089                is_mut: true,
8090                pattern: Box::new(bind_pat(2, "x")),
8091                ty: Some(Box::new(node(
8092                    3,
8093                    NodeKind::TypeNamed {
8094                        path: type_path(&["Int"]),
8095                        args: vec![],
8096                    },
8097                ))),
8098                value: Box::new(int_lit(4, "42")),
8099            },
8100        );
8101        let m = module(
8102            vec![],
8103            vec![node(
8104                5,
8105                NodeKind::FnDecl {
8106                    annotations: vec![],
8107                    visibility: Visibility::Private,
8108                    is_async: false,
8109                    name: ident("test"),
8110                    generic_params: vec![],
8111                    params: vec![],
8112                    return_type: None,
8113                    effect_clause: vec![],
8114                    where_clause: vec![],
8115                    body: Box::new(block(6, vec![let_node], None)),
8116                },
8117            )],
8118        );
8119        let out = gen(&m);
8120        assert!(out.contains("let mut x: i64 = 42_i64;"), "got: {out}");
8121    }
8122
8123    #[test]
8124    fn match_expression() {
8125        let m_node = node(
8126            1,
8127            NodeKind::Match {
8128                scrutinee: Box::new(id_node(2, "color")),
8129                arms: vec![
8130                    node(
8131                        3,
8132                        NodeKind::MatchArm {
8133                            pattern: Box::new(node(
8134                                4,
8135                                NodeKind::ConstructorPat {
8136                                    path: type_path(&["Color", "Red"]),
8137                                    fields: vec![],
8138                                },
8139                            )),
8140                            guard: None,
8141                            body: Box::new(block(5, vec![], Some(str_lit(6, "red")))),
8142                        },
8143                    ),
8144                    node(
8145                        7,
8146                        NodeKind::MatchArm {
8147                            pattern: Box::new(node(8, NodeKind::WildcardPat)),
8148                            guard: None,
8149                            body: Box::new(block(9, vec![], Some(str_lit(10, "other")))),
8150                        },
8151                    ),
8152                ],
8153            },
8154        );
8155        let f = node(
8156            11,
8157            NodeKind::FnDecl {
8158                annotations: vec![],
8159                visibility: Visibility::Private,
8160                is_async: false,
8161                name: ident("test"),
8162                generic_params: vec![],
8163                params: vec![],
8164                return_type: None,
8165                effect_clause: vec![],
8166                where_clause: vec![],
8167                body: Box::new(block(12, vec![m_node], None)),
8168            },
8169        );
8170        let out = gen(&module(vec![], vec![f]));
8171        assert!(out.contains("match color"), "got: {out}");
8172        assert!(out.contains("Color::Red =>"), "got: {out}");
8173        assert!(out.contains("_ =>"), "got: {out}");
8174    }
8175
8176    /// Build a `String`-typed param `fn <name>(s: String) -> String { match s { <arms> } }`.
8177    fn str_match_fn(name: &str, arms: Vec<AIRNode>) -> AIRNode {
8178        let m = node(
8179            900,
8180            NodeKind::Match {
8181                scrutinee: Box::new(id_node(901, "s")),
8182                arms,
8183            },
8184        );
8185        node(
8186            910,
8187            NodeKind::FnDecl {
8188                annotations: vec![],
8189                visibility: Visibility::Public,
8190                is_async: false,
8191                name: ident(name),
8192                generic_params: vec![],
8193                params: vec![typed_param_node(911, "s", "String")],
8194                return_type: Some(Box::new(node(
8195                    913,
8196                    NodeKind::TypeNamed {
8197                        path: type_path(&["String"]),
8198                        args: vec![],
8199                    },
8200                ))),
8201                effect_clause: vec![],
8202                where_clause: vec![],
8203                body: Box::new(block(912, vec![m], None)),
8204            },
8205        )
8206    }
8207
8208    fn str_lit_pat(id: u32, val: &str) -> AIRNode {
8209        node(
8210            id,
8211            NodeKind::LiteralPat {
8212                lit: Literal::String(val.into()),
8213            },
8214        )
8215    }
8216
8217    fn arm(id: u32, pattern: AIRNode, body: AIRNode) -> AIRNode {
8218        node(
8219            id,
8220            NodeKind::MatchArm {
8221                pattern: Box::new(pattern),
8222                guard: None,
8223                body: Box::new(body),
8224            },
8225        )
8226    }
8227
8228    /// A `String` scrutinee matched against `&str` literal arms must match on
8229    /// `(s).as_str()` so the literal patterns (`"hello"`, type `&str`) line up
8230    /// with the scrutinee (E0308 otherwise: `String` vs `&str`).
8231    #[test]
8232    fn rust_str_literal_match_uses_as_str() {
8233        let f = str_match_fn(
8234            "classify_string",
8235            vec![
8236                arm(
8237                    20,
8238                    str_lit_pat(21, "hello"),
8239                    block(22, vec![], Some(str_lit(23, "greeting"))),
8240                ),
8241                arm(
8242                    24,
8243                    str_lit_pat(25, "bye"),
8244                    block(26, vec![], Some(str_lit(27, "farewell"))),
8245                ),
8246                arm(
8247                    28,
8248                    node(29, NodeKind::WildcardPat),
8249                    block(30, vec![], Some(str_lit(31, "unknown"))),
8250                ),
8251            ],
8252        );
8253        let out = gen(&module(vec![], vec![f]));
8254        assert!(out.contains("match (s).as_str()"), "got: {out}");
8255        // And the whole module must compile (no E0308).
8256        assert!(
8257            check_rs_syntax(&out),
8258            "generated rust did not compile: {out}"
8259        );
8260    }
8261
8262    /// Guard against over-broadening: a `String` scrutinee with no string-literal
8263    /// arms (here a bare binding arm) must NOT be `.as_str()`-wrapped — that would
8264    /// rebind the value as `&str` and change its type.
8265    #[test]
8266    fn rust_str_literal_match_non_literal_unchanged() {
8267        let f = str_match_fn(
8268            "echo_string",
8269            vec![arm(
8270                40,
8271                bind_pat(41, "other"),
8272                block(42, vec![], Some(id_node(43, "other"))),
8273            )],
8274        );
8275        let out = gen(&module(vec![], vec![f]));
8276        assert!(!out.contains(".as_str()"), "should not wrap: {out}");
8277        assert!(out.contains("match s"), "got: {out}");
8278        assert!(
8279            check_rs_syntax(&out),
8280            "generated rust did not compile: {out}"
8281        );
8282    }
8283
8284    /// Q-rust-str-mixed-binding: a `String` match mixing string-literal arms with
8285    /// a top-level *binding* arm (`other => other`) must still match on
8286    /// `(s).as_str()` (so the `&str` literal arm typechecks), AND re-bind the
8287    /// `&str` whole-scrutinee bind back to an owned `String` at the top of its arm
8288    /// body (`let other = other.to_string();`) so the body keeps its `String`
8289    /// binding. The whole module must compile (no E0308 on the literal arm, no
8290    /// type mismatch on the returned `other`).
8291    #[test]
8292    fn rust_str_literal_match_with_binding_arm_rebinds() {
8293        let f = str_match_fn(
8294            "describe_string",
8295            vec![
8296                arm(
8297                    44,
8298                    str_lit_pat(45, "hi"),
8299                    block(46, vec![], Some(str_lit(47, "greeting"))),
8300                ),
8301                arm(
8302                    48,
8303                    bind_pat(49, "other"),
8304                    block(70, vec![], Some(id_node(71, "other"))),
8305                ),
8306            ],
8307        );
8308        let out = gen(&module(vec![], vec![f]));
8309        assert!(
8310            out.contains("match (s).as_str()"),
8311            "mixed literal/bind match must still wrap with .as_str(): {out}"
8312        );
8313        assert!(
8314            out.contains("let other = other.to_string();"),
8315            "bind arm must re-bind &str → String: {out}"
8316        );
8317        // And the whole module must compile (no E0308, no String/&str mismatch).
8318        assert!(
8319            check_rs_syntax(&out),
8320            "generated rust did not compile: {out}"
8321        );
8322    }
8323
8324    /// The `.as_str()` wrapping must apply in expression position too (a `match`
8325    /// used as the value of an enclosing expression), not only statement position.
8326    #[test]
8327    fn rust_str_literal_match_expr_position() {
8328        // fn label(s: String) -> String { let r: String = match s { "y" => "yes", _ => "no" }; r }
8329        let m = node(
8330            50,
8331            NodeKind::Match {
8332                scrutinee: Box::new(id_node(51, "s")),
8333                arms: vec![
8334                    arm(
8335                        52,
8336                        str_lit_pat(53, "y"),
8337                        block(54, vec![], Some(str_lit(55, "yes"))),
8338                    ),
8339                    arm(
8340                        56,
8341                        node(57, NodeKind::WildcardPat),
8342                        block(58, vec![], Some(str_lit(59, "no"))),
8343                    ),
8344                ],
8345            },
8346        );
8347        let let_node = node(
8348            60,
8349            NodeKind::LetBinding {
8350                is_mut: false,
8351                pattern: Box::new(bind_pat(61, "r")),
8352                ty: Some(Box::new(node(
8353                    62,
8354                    NodeKind::TypeNamed {
8355                        path: type_path(&["String"]),
8356                        args: vec![],
8357                    },
8358                ))),
8359                value: Box::new(m),
8360            },
8361        );
8362        let f = node(
8363            63,
8364            NodeKind::FnDecl {
8365                annotations: vec![],
8366                visibility: Visibility::Public,
8367                is_async: false,
8368                name: ident("label"),
8369                generic_params: vec![],
8370                params: vec![typed_param_node(64, "s", "String")],
8371                return_type: Some(Box::new(node(
8372                    67,
8373                    NodeKind::TypeNamed {
8374                        path: type_path(&["String"]),
8375                        args: vec![],
8376                    },
8377                ))),
8378                effect_clause: vec![],
8379                where_clause: vec![],
8380                body: Box::new(block(65, vec![let_node], Some(id_node(66, "r")))),
8381            },
8382        );
8383        let out = gen(&module(vec![], vec![f]));
8384        assert!(out.contains("match (s).as_str()"), "got: {out}");
8385        assert!(
8386            check_rs_syntax(&out),
8387            "generated rust did not compile: {out}"
8388        );
8389    }
8390
8391    #[test]
8392    fn string_interpolation() {
8393        let interp = node(
8394            1,
8395            NodeKind::Interpolation {
8396                parts: vec![
8397                    AirInterpolationPart::Literal("Hello, ".into()),
8398                    AirInterpolationPart::Expr(Box::new(id_node(2, "name"))),
8399                    AirInterpolationPart::Literal("!".into()),
8400                ],
8401            },
8402        );
8403        let f = node(
8404            3,
8405            NodeKind::FnDecl {
8406                annotations: vec![],
8407                visibility: Visibility::Private,
8408                is_async: false,
8409                name: ident("test"),
8410                generic_params: vec![],
8411                params: vec![],
8412                return_type: None,
8413                effect_clause: vec![],
8414                where_clause: vec![],
8415                body: Box::new(block(4, vec![], Some(interp))),
8416            },
8417        );
8418        let out = gen(&module(vec![], vec![f]));
8419        assert!(out.contains("format!(\"Hello, {}!\", name)"), "got: {out}");
8420    }
8421
8422    #[test]
8423    fn result_construct() {
8424        let ok = node(
8425            1,
8426            NodeKind::ResultConstruct {
8427                variant: ResultVariant::Ok,
8428                value: Some(Box::new(int_lit(2, "42"))),
8429            },
8430        );
8431        let err = node(
8432            3,
8433            NodeKind::ResultConstruct {
8434                variant: ResultVariant::Err,
8435                value: Some(Box::new(str_lit(4, "oops"))),
8436            },
8437        );
8438        let f = node(
8439            5,
8440            NodeKind::FnDecl {
8441                annotations: vec![],
8442                visibility: Visibility::Private,
8443                is_async: false,
8444                name: ident("test"),
8445                generic_params: vec![],
8446                params: vec![],
8447                return_type: None,
8448                effect_clause: vec![],
8449                where_clause: vec![],
8450                body: Box::new(block(6, vec![], Some(ok))),
8451            },
8452        );
8453        let out = gen(&module(vec![], vec![f]));
8454        assert!(out.contains("Ok(42_i64)"), "got: {out}");
8455
8456        let f2 = node(
8457            7,
8458            NodeKind::FnDecl {
8459                annotations: vec![],
8460                visibility: Visibility::Private,
8461                is_async: false,
8462                name: ident("test2"),
8463                generic_params: vec![],
8464                params: vec![],
8465                return_type: None,
8466                effect_clause: vec![],
8467                where_clause: vec![],
8468                body: Box::new(block(8, vec![], Some(err))),
8469            },
8470        );
8471        let out2 = gen(&module(vec![], vec![f2]));
8472        assert!(out2.contains("Err(\"oops\".to_string())"), "got: {out2}");
8473    }
8474
8475    #[test]
8476    fn vec_literal() {
8477        let list = node(
8478            1,
8479            NodeKind::ListLiteral {
8480                elems: vec![int_lit(2, "1"), int_lit(3, "2"), int_lit(4, "3")],
8481            },
8482        );
8483        let f = node(
8484            5,
8485            NodeKind::FnDecl {
8486                annotations: vec![],
8487                visibility: Visibility::Private,
8488                is_async: false,
8489                name: ident("test"),
8490                generic_params: vec![],
8491                params: vec![],
8492                return_type: None,
8493                effect_clause: vec![],
8494                where_clause: vec![],
8495                body: Box::new(block(6, vec![], Some(list))),
8496            },
8497        );
8498        let out = gen(&module(vec![], vec![f]));
8499        assert!(out.contains("vec![1_i64, 2_i64, 3_i64]"), "got: {out}");
8500    }
8501
8502    #[test]
8503    fn propagate_operator() {
8504        let prop = node(
8505            1,
8506            NodeKind::Propagate {
8507                expr: Box::new(node(
8508                    2,
8509                    NodeKind::Call {
8510                        callee: Box::new(id_node(3, "parse")),
8511                        args: vec![],
8512                        type_args: vec![],
8513                    },
8514                )),
8515            },
8516        );
8517        let f = node(
8518            4,
8519            NodeKind::FnDecl {
8520                annotations: vec![],
8521                visibility: Visibility::Private,
8522                is_async: false,
8523                name: ident("test"),
8524                generic_params: vec![],
8525                params: vec![],
8526                return_type: None,
8527                effect_clause: vec![],
8528                where_clause: vec![],
8529                body: Box::new(block(5, vec![], Some(prop))),
8530            },
8531        );
8532        let out = gen(&module(vec![], vec![f]));
8533        assert!(out.contains("parse()?"), "got: {out}");
8534    }
8535
8536    #[test]
8537    fn range_expression() {
8538        let range = node(
8539            1,
8540            NodeKind::Range {
8541                lo: Box::new(int_lit(2, "0")),
8542                hi: Box::new(int_lit(3, "10")),
8543                inclusive: false,
8544            },
8545        );
8546        let range_incl = node(
8547            4,
8548            NodeKind::Range {
8549                lo: Box::new(int_lit(5, "0")),
8550                hi: Box::new(int_lit(6, "10")),
8551                inclusive: true,
8552            },
8553        );
8554        let f = node(
8555            7,
8556            NodeKind::FnDecl {
8557                annotations: vec![],
8558                visibility: Visibility::Private,
8559                is_async: false,
8560                name: ident("test"),
8561                generic_params: vec![],
8562                params: vec![],
8563                return_type: None,
8564                effect_clause: vec![],
8565                where_clause: vec![],
8566                body: Box::new(block(8, vec![], Some(range))),
8567            },
8568        );
8569        let out = gen(&module(vec![], vec![f]));
8570        assert!(out.contains("0_i64..10_i64"), "got: {out}");
8571
8572        let f2 = node(
8573            9,
8574            NodeKind::FnDecl {
8575                annotations: vec![],
8576                visibility: Visibility::Private,
8577                is_async: false,
8578                name: ident("test2"),
8579                generic_params: vec![],
8580                params: vec![],
8581                return_type: None,
8582                effect_clause: vec![],
8583                where_clause: vec![],
8584                body: Box::new(block(10, vec![], Some(range_incl))),
8585            },
8586        );
8587        let out2 = gen(&module(vec![], vec![f2]));
8588        assert!(out2.contains("0_i64..=10_i64"), "got: {out2}");
8589    }
8590
8591    #[test]
8592    fn generics_with_bounds() {
8593        let f = node(
8594            1,
8595            NodeKind::FnDecl {
8596                annotations: vec![],
8597                visibility: Visibility::Public,
8598                is_async: false,
8599                name: ident("show"),
8600                generic_params: vec![GenericParam {
8601                    id: 100,
8602                    span: span(),
8603                    name: ident("T"),
8604                    bounds: vec![type_path(&["Display"])],
8605                }],
8606                params: vec![typed_param_node(2, "val", "T")],
8607                return_type: Some(Box::new(node(
8608                    3,
8609                    NodeKind::TypeNamed {
8610                        path: type_path(&["String"]),
8611                        args: vec![],
8612                    },
8613                ))),
8614                effect_clause: vec![],
8615                where_clause: vec![],
8616                body: Box::new(block(4, vec![], Some(id_node(5, "val")))),
8617            },
8618        );
8619        let out = gen(&module(vec![], vec![f]));
8620        assert!(
8621            out.contains("pub fn show<T: Display>(val: T) -> String"),
8622            "got: {out}"
8623        );
8624    }
8625
8626    #[test]
8627    fn type_alias() {
8628        let alias = node(
8629            1,
8630            NodeKind::TypeAlias {
8631                annotations: vec![],
8632                visibility: Visibility::Public,
8633                name: ident("Coord"),
8634                generic_params: vec![],
8635                ty: Box::new(node(
8636                    2,
8637                    NodeKind::TypeTuple {
8638                        elems: vec![
8639                            node(
8640                                3,
8641                                NodeKind::TypeNamed {
8642                                    path: type_path(&["Float"]),
8643                                    args: vec![],
8644                                },
8645                            ),
8646                            node(
8647                                4,
8648                                NodeKind::TypeNamed {
8649                                    path: type_path(&["Float"]),
8650                                    args: vec![],
8651                                },
8652                            ),
8653                        ],
8654                    },
8655                )),
8656                where_clause: vec![],
8657            },
8658        );
8659        let out = gen(&module(vec![], vec![alias]));
8660        assert!(out.contains("pub type Coord = (f64, f64);"), "got: {out}");
8661    }
8662
8663    #[test]
8664    fn const_declaration() {
8665        let c = node(
8666            1,
8667            NodeKind::ConstDecl {
8668                annotations: vec![],
8669                visibility: Visibility::Public,
8670                name: ident("MaxSize"),
8671                ty: Box::new(node(
8672                    2,
8673                    NodeKind::TypeNamed {
8674                        path: type_path(&["Int"]),
8675                        args: vec![],
8676                    },
8677                )),
8678                value: Box::new(int_lit(3, "100")),
8679            },
8680        );
8681        let out = gen(&module(vec![], vec![c]));
8682        assert!(
8683            out.contains("pub const MAX_SIZE: i64 = 100_i64;"),
8684            "got: {out}"
8685        );
8686    }
8687
8688    #[test]
8689    fn import_declaration_is_dropped() {
8690        // In the single-module self-contained emit (`generate_module`), there is
8691        // no sibling module to import from, so a Bock `ImportDecl` emits nothing.
8692        // (The per-module project path emits real `use crate::<m>::<x>;`.)
8693        let imp = node(
8694            1,
8695            NodeKind::ImportDecl {
8696                path: mod_path(&["core", "compare"]),
8697                items: ImportItems::Named(vec![imported_name("Key"), imported_name("key")]),
8698            },
8699        );
8700        let out = gen(&module(vec![imp], vec![]));
8701        assert!(
8702            !out.contains("use core::compare"),
8703            "ImportDecl must be a no-op in single-module emit; got: {out}"
8704        );
8705    }
8706
8707    #[test]
8708    fn for_loop() {
8709        let body = block(
8710            3,
8711            vec![node(
8712                4,
8713                NodeKind::LetBinding {
8714                    is_mut: false,
8715                    pattern: Box::new(bind_pat(5, "y")),
8716                    ty: None,
8717                    value: Box::new(id_node(6, "x")),
8718                },
8719            )],
8720            None,
8721        );
8722        let for_node = node(
8723            1,
8724            NodeKind::For {
8725                pattern: Box::new(bind_pat(2, "x")),
8726                iterable: Box::new(id_node(7, "items")),
8727                body: Box::new(body),
8728            },
8729        );
8730        let f = node(
8731            8,
8732            NodeKind::FnDecl {
8733                annotations: vec![],
8734                visibility: Visibility::Private,
8735                is_async: false,
8736                name: ident("test"),
8737                generic_params: vec![],
8738                params: vec![],
8739                return_type: None,
8740                effect_clause: vec![],
8741                where_clause: vec![],
8742                body: Box::new(block(9, vec![for_node], None)),
8743            },
8744        );
8745        let out = gen(&module(vec![], vec![f]));
8746        assert!(out.contains("for x in items {"), "got: {out}");
8747        assert!(out.contains("let y = x;"), "got: {out}");
8748    }
8749
8750    #[test]
8751    fn await_expression() {
8752        let aw = node(
8753            1,
8754            NodeKind::Await {
8755                expr: Box::new(node(
8756                    2,
8757                    NodeKind::Call {
8758                        callee: Box::new(id_node(3, "fetch")),
8759                        args: vec![],
8760                        type_args: vec![],
8761                    },
8762                )),
8763            },
8764        );
8765        let f = node(
8766            4,
8767            NodeKind::FnDecl {
8768                annotations: vec![],
8769                visibility: Visibility::Private,
8770                is_async: true,
8771                name: ident("test"),
8772                generic_params: vec![],
8773                params: vec![],
8774                return_type: None,
8775                effect_clause: vec![],
8776                where_clause: vec![],
8777                body: Box::new(block(5, vec![], Some(aw))),
8778            },
8779        );
8780        let out = gen(&module(vec![], vec![f]));
8781        assert!(out.contains("async fn test()"), "got: {out}");
8782        assert!(out.contains("fetch().await"), "got: {out}");
8783    }
8784
8785    #[test]
8786    fn async_main_gets_tokio_main_attribute() {
8787        let body = block(2, vec![], None);
8788        let f = node(
8789            1,
8790            NodeKind::FnDecl {
8791                annotations: vec![],
8792                visibility: Visibility::Private,
8793                is_async: true,
8794                name: ident("main"),
8795                generic_params: vec![],
8796                params: vec![],
8797                return_type: None,
8798                effect_clause: vec![],
8799                where_clause: vec![],
8800                body: Box::new(body),
8801            },
8802        );
8803        let out = gen(&module(vec![], vec![f]));
8804        assert!(out.contains("#[tokio::main]"), "got: {out}");
8805        assert!(out.contains("async fn main()"), "got: {out}");
8806    }
8807
8808    #[test]
8809    fn sync_main_no_tokio_attribute() {
8810        let body = block(2, vec![], None);
8811        let f = node(
8812            1,
8813            NodeKind::FnDecl {
8814                annotations: vec![],
8815                visibility: Visibility::Private,
8816                is_async: false,
8817                name: ident("main"),
8818                generic_params: vec![],
8819                params: vec![],
8820                return_type: None,
8821                effect_clause: vec![],
8822                where_clause: vec![],
8823                body: Box::new(body),
8824            },
8825        );
8826        let out = gen(&module(vec![], vec![f]));
8827        assert!(!out.contains("#[tokio::main]"), "got: {out}");
8828        assert!(out.contains("fn main()"), "got: {out}");
8829    }
8830
8831    #[test]
8832    fn concurrent_pattern_spawns_tasks() {
8833        // Two async calls bound to locals, then awaited later in same block —
8834        // should wrap each in `tokio::spawn` and unwrap JoinHandles on await.
8835        let call_fetch = |id: u32, name: &str| {
8836            node(
8837                id,
8838                NodeKind::Call {
8839                    callee: Box::new(id_node(id + 1, name)),
8840                    args: vec![],
8841                    type_args: vec![],
8842                },
8843            )
8844        };
8845        let let_stmt = |id: u32, name: &str, val: AIRNode| {
8846            node(
8847                id,
8848                NodeKind::LetBinding {
8849                    is_mut: false,
8850                    pattern: Box::new(bind_pat(id + 1, name)),
8851                    ty: None,
8852                    value: Box::new(val),
8853                },
8854            )
8855        };
8856        let await_id = |id: u32, name: &str| {
8857            node(
8858                id,
8859                NodeKind::Await {
8860                    expr: Box::new(id_node(id + 1, name)),
8861                },
8862            )
8863        };
8864        let body = block(
8865            10,
8866            vec![
8867                let_stmt(20, "a", call_fetch(21, "task1")),
8868                let_stmt(30, "b", call_fetch(31, "task2")),
8869                let_stmt(40, "ra", await_id(41, "a")),
8870                let_stmt(50, "rb", await_id(51, "b")),
8871            ],
8872            Some(id_node(60, "ra")),
8873        );
8874        let f = node(
8875            1,
8876            NodeKind::FnDecl {
8877                annotations: vec![],
8878                visibility: Visibility::Private,
8879                is_async: true,
8880                name: ident("run"),
8881                generic_params: vec![],
8882                params: vec![],
8883                return_type: None,
8884                effect_clause: vec![],
8885                where_clause: vec![],
8886                body: Box::new(body),
8887            },
8888        );
8889        let out = gen(&module(vec![], vec![f]));
8890        assert!(
8891            out.contains("let a = tokio::spawn(task1());"),
8892            "task1 should be spawned, got: {out}"
8893        );
8894        assert!(
8895            out.contains("let b = tokio::spawn(task2());"),
8896            "task2 should be spawned, got: {out}"
8897        );
8898        assert!(
8899            out.contains("let ra = a.await.unwrap();"),
8900            "join handle `a` should be unwrapped on await, got: {out}"
8901        );
8902        assert!(
8903            out.contains("let rb = b.await.unwrap();"),
8904            "join handle `b` should be unwrapped on await, got: {out}"
8905        );
8906    }
8907
8908    #[test]
8909    fn sequential_await_no_spawn() {
8910        // `let a = await task1()` directly awaits — no spawn wrap.
8911        let await_call = node(
8912            20,
8913            NodeKind::Await {
8914                expr: Box::new(node(
8915                    21,
8916                    NodeKind::Call {
8917                        callee: Box::new(id_node(22, "task1")),
8918                        args: vec![],
8919                        type_args: vec![],
8920                    },
8921                )),
8922            },
8923        );
8924        let let_stmt = node(
8925            10,
8926            NodeKind::LetBinding {
8927                is_mut: false,
8928                pattern: Box::new(bind_pat(11, "a")),
8929                ty: None,
8930                value: Box::new(await_call),
8931            },
8932        );
8933        let body = block(30, vec![let_stmt], Some(id_node(40, "a")));
8934        let f = node(
8935            1,
8936            NodeKind::FnDecl {
8937                annotations: vec![],
8938                visibility: Visibility::Private,
8939                is_async: true,
8940                name: ident("run"),
8941                generic_params: vec![],
8942                params: vec![],
8943                return_type: None,
8944                effect_clause: vec![],
8945                where_clause: vec![],
8946                body: Box::new(body),
8947            },
8948        );
8949        let out = gen(&module(vec![], vec![f]));
8950        assert!(
8951            !out.contains("tokio::spawn"),
8952            "sequential await should not spawn, got: {out}"
8953        );
8954        assert!(out.contains("let a = task1().await;"), "got: {out}");
8955    }
8956
8957    #[test]
8958    fn record_construct() {
8959        let rc = node(
8960            1,
8961            NodeKind::RecordConstruct {
8962                path: type_path(&["Point"]),
8963                fields: vec![
8964                    AirRecordField {
8965                        name: ident("x"),
8966                        value: Some(Box::new(int_lit(2, "1"))),
8967                    },
8968                    AirRecordField {
8969                        name: ident("y"),
8970                        value: Some(Box::new(int_lit(3, "2"))),
8971                    },
8972                ],
8973                spread: None,
8974            },
8975        );
8976        let f = node(
8977            4,
8978            NodeKind::FnDecl {
8979                annotations: vec![],
8980                visibility: Visibility::Private,
8981                is_async: false,
8982                name: ident("test"),
8983                generic_params: vec![],
8984                params: vec![],
8985                return_type: None,
8986                effect_clause: vec![],
8987                where_clause: vec![],
8988                body: Box::new(block(5, vec![], Some(rc))),
8989            },
8990        );
8991        let out = gen(&module(vec![], vec![f]));
8992        assert!(out.contains("Point { x: 1_i64, y: 2_i64 }"), "got: {out}");
8993    }
8994
8995    #[test]
8996    fn map_literal() {
8997        let map = node(
8998            1,
8999            NodeKind::MapLiteral {
9000                entries: vec![AirMapEntry {
9001                    key: str_lit(2, "key"),
9002                    value: int_lit(3, "42"),
9003                }],
9004            },
9005        );
9006        let f = node(
9007            4,
9008            NodeKind::FnDecl {
9009                annotations: vec![],
9010                visibility: Visibility::Private,
9011                is_async: false,
9012                name: ident("test"),
9013                generic_params: vec![],
9014                params: vec![],
9015                return_type: None,
9016                effect_clause: vec![],
9017                where_clause: vec![],
9018                body: Box::new(block(5, vec![], Some(map))),
9019            },
9020        );
9021        let out = gen(&module(vec![], vec![f]));
9022        assert!(
9023            out.contains("std::collections::HashMap::from([(\"key\".to_string(), 42_i64)])"),
9024            "got: {out}"
9025        );
9026    }
9027
9028    #[test]
9029    fn tuple_literal() {
9030        let tup = node(
9031            1,
9032            NodeKind::TupleLiteral {
9033                elems: vec![int_lit(2, "1"), str_lit(3, "hello"), bool_lit(4, true)],
9034            },
9035        );
9036        let f = node(
9037            5,
9038            NodeKind::FnDecl {
9039                annotations: vec![],
9040                visibility: Visibility::Private,
9041                is_async: false,
9042                name: ident("test"),
9043                generic_params: vec![],
9044                params: vec![],
9045                return_type: None,
9046                effect_clause: vec![],
9047                where_clause: vec![],
9048                body: Box::new(block(6, vec![], Some(tup))),
9049            },
9050        );
9051        let out = gen(&module(vec![], vec![f]));
9052        assert!(
9053            out.contains("(1_i64, \"hello\".to_string(), true)"),
9054            "got: {out}"
9055        );
9056    }
9057
9058    #[test]
9059    fn unreachable_expression() {
9060        let unr = node(1, NodeKind::Unreachable);
9061        let f = node(
9062            2,
9063            NodeKind::FnDecl {
9064                annotations: vec![],
9065                visibility: Visibility::Private,
9066                is_async: false,
9067                name: ident("test"),
9068                generic_params: vec![],
9069                params: vec![],
9070                return_type: None,
9071                effect_clause: vec![],
9072                where_clause: vec![],
9073                body: Box::new(block(3, vec![], Some(unr))),
9074            },
9075        );
9076        let out = gen(&module(vec![], vec![f]));
9077        assert!(out.contains("unreachable!()"), "got: {out}");
9078    }
9079
9080    #[test]
9081    fn escape_strings() {
9082        assert_eq!(escape_rs_string("hello"), "hello");
9083        assert_eq!(escape_rs_string("he\"llo"), "he\\\"llo");
9084        assert_eq!(escape_rs_string("new\nline"), "new\\nline");
9085    }
9086
9087    #[test]
9088    fn escape_format_strings() {
9089        assert_eq!(escape_format_string("hello"), "hello");
9090        assert_eq!(escape_format_string("{test}"), "{{test}}");
9091    }
9092
9093    #[test]
9094    fn to_snake_case_conversions() {
9095        assert_eq!(to_snake_case("hello"), "hello");
9096        assert_eq!(to_snake_case("HelloWorld"), "hello_world");
9097        assert_eq!(to_snake_case("camelCase"), "camel_case");
9098        assert_eq!(to_snake_case("HTTPClient"), "http_client");
9099        assert_eq!(to_snake_case("_"), "_");
9100    }
9101
9102    #[test]
9103    fn to_upper_snake_case_conversions() {
9104        assert_eq!(to_upper_snake_case("MaxSize"), "MAX_SIZE");
9105        assert_eq!(to_upper_snake_case("httpClient"), "HTTP_CLIENT");
9106    }
9107
9108    // ── End-to-end syntax validation tests ──────────────────────────────────
9109
9110    #[test]
9111    #[ignore]
9112    fn e2e_simple_function_compiles() {
9113        let body = block(2, vec![], Some(int_lit(3, "42")));
9114        let f = node(
9115            1,
9116            NodeKind::FnDecl {
9117                annotations: vec![],
9118                visibility: Visibility::Public,
9119                is_async: false,
9120                name: ident("answer"),
9121                generic_params: vec![],
9122                params: vec![],
9123                return_type: Some(Box::new(node(
9124                    4,
9125                    NodeKind::TypeNamed {
9126                        path: type_path(&["Int"]),
9127                        args: vec![],
9128                    },
9129                ))),
9130                effect_clause: vec![],
9131                where_clause: vec![],
9132                body: Box::new(body),
9133            },
9134        );
9135        let out = gen(&module(vec![], vec![f]));
9136        assert!(
9137            check_rs_syntax(&out),
9138            "Generated Rust does not compile:\n{out}"
9139        );
9140    }
9141
9142    #[test]
9143    #[ignore]
9144    fn e2e_struct_compiles() {
9145        let record = node(
9146            1,
9147            NodeKind::RecordDecl {
9148                annotations: vec![],
9149                visibility: Visibility::Public,
9150                name: ident("Point"),
9151                generic_params: vec![],
9152                fields: vec![record_field("x", "Float"), record_field("y", "Float")],
9153            },
9154        );
9155        let out = gen(&module(vec![], vec![record]));
9156        assert!(
9157            check_rs_syntax(&out),
9158            "Generated Rust does not compile:\n{out}"
9159        );
9160    }
9161
9162    #[test]
9163    #[ignore]
9164    fn e2e_enum_compiles() {
9165        let e = node(
9166            1,
9167            NodeKind::EnumDecl {
9168                annotations: vec![],
9169                visibility: Visibility::Public,
9170                name: ident("Color"),
9171                generic_params: vec![],
9172                variants: vec![
9173                    node(
9174                        2,
9175                        NodeKind::EnumVariant {
9176                            name: ident("Red"),
9177                            payload: EnumVariantPayload::Unit,
9178                        },
9179                    ),
9180                    node(
9181                        3,
9182                        NodeKind::EnumVariant {
9183                            name: ident("Rgb"),
9184                            payload: EnumVariantPayload::Struct(vec![record_field("r", "Int")]),
9185                        },
9186                    ),
9187                    node(
9188                        5,
9189                        NodeKind::EnumVariant {
9190                            name: ident("Custom"),
9191                            payload: EnumVariantPayload::Tuple(vec![node(
9192                                6,
9193                                NodeKind::TypeNamed {
9194                                    path: type_path(&["String"]),
9195                                    args: vec![],
9196                                },
9197                            )]),
9198                        },
9199                    ),
9200                ],
9201            },
9202        );
9203        let out = gen(&module(vec![], vec![e]));
9204        assert!(
9205            check_rs_syntax(&out),
9206            "Generated Rust does not compile:\n{out}"
9207        );
9208    }
9209
9210    #[test]
9211    #[ignore]
9212    fn e2e_trait_and_impl_compiles() {
9213        let trait_decl = node(
9214            1,
9215            NodeKind::TraitDecl {
9216                annotations: vec![],
9217                visibility: Visibility::Public,
9218                is_platform: false,
9219                name: ident("Greet"),
9220                generic_params: vec![],
9221                associated_types: vec![],
9222                methods: vec![node(
9223                    2,
9224                    NodeKind::FnDecl {
9225                        annotations: vec![],
9226                        visibility: Visibility::Public,
9227                        is_async: false,
9228                        name: ident("greet"),
9229                        generic_params: vec![],
9230                        params: vec![],
9231                        return_type: Some(Box::new(node(
9232                            3,
9233                            NodeKind::TypeNamed {
9234                                path: type_path(&["String"]),
9235                                args: vec![],
9236                            },
9237                        ))),
9238                        effect_clause: vec![],
9239                        where_clause: vec![],
9240                        body: Box::new(block(4, vec![], None)),
9241                    },
9242                )],
9243            },
9244        );
9245        let struct_decl = node(
9246            10,
9247            NodeKind::RecordDecl {
9248                annotations: vec![],
9249                visibility: Visibility::Public,
9250                name: ident("Person"),
9251                generic_params: vec![],
9252                fields: vec![record_field("name", "String")],
9253            },
9254        );
9255        let impl_block = node(
9256            20,
9257            NodeKind::ImplBlock {
9258                annotations: vec![],
9259                generic_params: vec![],
9260                trait_path: Some(type_path(&["Greet"])),
9261                trait_args: vec![],
9262                target: Box::new(node(
9263                    21,
9264                    NodeKind::TypeNamed {
9265                        path: type_path(&["Person"]),
9266                        args: vec![],
9267                    },
9268                )),
9269                where_clause: vec![],
9270                methods: vec![node(
9271                    22,
9272                    NodeKind::FnDecl {
9273                        annotations: vec![],
9274                        visibility: Visibility::Public,
9275                        is_async: false,
9276                        name: ident("greet"),
9277                        generic_params: vec![],
9278                        params: vec![],
9279                        return_type: Some(Box::new(node(
9280                            23,
9281                            NodeKind::TypeNamed {
9282                                path: type_path(&["String"]),
9283                                args: vec![],
9284                            },
9285                        ))),
9286                        effect_clause: vec![],
9287                        where_clause: vec![],
9288                        body: Box::new(block(24, vec![], Some(str_lit(25, "hello")))),
9289                    },
9290                )],
9291            },
9292        );
9293        let out = gen(&module(vec![], vec![trait_decl, struct_decl, impl_block]));
9294        assert!(
9295            check_rs_syntax(&out),
9296            "Generated Rust does not compile:\n{out}"
9297        );
9298    }
9299
9300    // ── Prelude function mapping tests ──────────────────────────────────────
9301
9302    /// Helper: generate Rust for a module with a `main` function containing a single call.
9303    fn gen_prelude_call(func_name: &str, arg: AIRNode) -> String {
9304        let call = node(
9305            10,
9306            NodeKind::Call {
9307                callee: Box::new(id_node(11, func_name)),
9308                args: vec![AirArg {
9309                    label: None,
9310                    value: arg,
9311                }],
9312                type_args: vec![],
9313            },
9314        );
9315        let body = block(2, vec![call], None);
9316        let f = node(
9317            1,
9318            NodeKind::FnDecl {
9319                name: ident("main"),
9320                params: vec![],
9321                return_type: None,
9322                body: Box::new(body),
9323                generic_params: vec![],
9324                visibility: Visibility::Private,
9325                annotations: vec![],
9326                effect_clause: vec![],
9327                where_clause: vec![],
9328                is_async: false,
9329            },
9330        );
9331        gen(&module(vec![], vec![f]))
9332    }
9333
9334    /// Helper: generate Rust for a nullary prelude call (no args).
9335    fn gen_prelude_call_no_args(func_name: &str) -> String {
9336        let call = node(
9337            10,
9338            NodeKind::Call {
9339                callee: Box::new(id_node(11, func_name)),
9340                args: vec![],
9341                type_args: vec![],
9342            },
9343        );
9344        let body = block(2, vec![call], None);
9345        let f = node(
9346            1,
9347            NodeKind::FnDecl {
9348                name: ident("main"),
9349                params: vec![],
9350                return_type: None,
9351                body: Box::new(body),
9352                generic_params: vec![],
9353                visibility: Visibility::Private,
9354                annotations: vec![],
9355                effect_clause: vec![],
9356                where_clause: vec![],
9357                is_async: false,
9358            },
9359        );
9360        gen(&module(vec![], vec![f]))
9361    }
9362
9363    #[test]
9364    fn prelude_println_maps_to_println_macro() {
9365        let out = gen_prelude_call("println", str_lit(12, "hello"));
9366        assert!(
9367            out.contains("println!(\"{}\", "),
9368            "println should map to println! macro, got: {out}"
9369        );
9370        assert!(
9371            !out.contains("println("),
9372            "should not emit bare println(, got: {out}"
9373        );
9374    }
9375
9376    #[test]
9377    fn prelude_print_maps_to_print_macro() {
9378        let out = gen_prelude_call("print", str_lit(12, "hello"));
9379        assert!(
9380            out.contains("print!(\"{}\", "),
9381            "print should map to print! macro, got: {out}"
9382        );
9383    }
9384
9385    #[test]
9386    fn prelude_debug_maps_to_dbg_macro() {
9387        let out = gen_prelude_call("debug", str_lit(12, "val"));
9388        assert!(
9389            out.contains("dbg!(&"),
9390            "debug should map to dbg! macro, got: {out}"
9391        );
9392    }
9393
9394    #[test]
9395    fn prelude_assert_maps_to_assert_macro() {
9396        let out = gen_prelude_call("assert", bool_lit(12, true));
9397        assert!(
9398            out.contains("assert!("),
9399            "assert should map to assert! macro, got: {out}"
9400        );
9401    }
9402
9403    #[test]
9404    fn prelude_todo_maps_to_todo_macro() {
9405        let out = gen_prelude_call_no_args("todo");
9406        assert!(
9407            out.contains("todo!()"),
9408            "todo should map to todo! macro, got: {out}"
9409        );
9410    }
9411
9412    #[test]
9413    fn prelude_unreachable_maps_to_unreachable_macro() {
9414        let out = gen_prelude_call_no_args("unreachable");
9415        assert!(
9416            out.contains("unreachable!()"),
9417            "unreachable should map to unreachable! macro, got: {out}"
9418        );
9419    }
9420
9421    #[test]
9422    fn non_prelude_call_passes_through() {
9423        let out = gen_prelude_call("my_custom_func", str_lit(12, "arg"));
9424        assert!(
9425            out.contains("my_custom_func("),
9426            "non-prelude call should use snake_case, got: {out}"
9427        );
9428    }
9429
9430    #[test]
9431    fn handling_block_passes_handlers_to_effectful_call() {
9432        use bock_air::AirHandlerPair;
9433
9434        // effect Logger { fn log(msg: String) -> Void }
9435        let effect_decl = node(
9436            1,
9437            NodeKind::EffectDecl {
9438                annotations: vec![],
9439                visibility: Visibility::Public,
9440                name: ident("Logger"),
9441                generic_params: vec![],
9442                components: vec![],
9443                operations: vec![node(
9444                    2,
9445                    NodeKind::FnDecl {
9446                        annotations: vec![],
9447                        visibility: Visibility::Public,
9448                        is_async: false,
9449                        name: ident("log"),
9450                        generic_params: vec![],
9451                        params: vec![typed_param_node(3, "msg", "String")],
9452                        return_type: None,
9453                        effect_clause: vec![],
9454                        where_clause: vec![],
9455                        body: Box::new(block(4, vec![], None)),
9456                    },
9457                )],
9458            },
9459        );
9460
9461        // fn inner() -> String with Logger { ... }
9462        let inner_fn = node(
9463            10,
9464            NodeKind::FnDecl {
9465                annotations: vec![],
9466                visibility: Visibility::Private,
9467                is_async: false,
9468                name: ident("inner"),
9469                generic_params: vec![],
9470                params: vec![],
9471                return_type: Some(Box::new(node(
9472                    11,
9473                    NodeKind::TypeNamed {
9474                        path: type_path(&["String"]),
9475                        args: vec![],
9476                    },
9477                ))),
9478                effect_clause: vec![type_path(&["Logger"])],
9479                where_clause: vec![],
9480                body: Box::new(block(12, vec![], Some(str_lit(13, "hello")))),
9481            },
9482        );
9483
9484        // fn main() { handling (Logger with StdoutLogger {}) { inner() } }
9485        let call_inner = node(
9486            20,
9487            NodeKind::Call {
9488                callee: Box::new(id_node(21, "inner")),
9489                args: vec![],
9490                type_args: vec![],
9491            },
9492        );
9493        let handling = node(
9494            30,
9495            NodeKind::HandlingBlock {
9496                handlers: vec![AirHandlerPair {
9497                    effect: type_path(&["Logger"]),
9498                    handler: Box::new(node(
9499                        31,
9500                        NodeKind::Call {
9501                            callee: Box::new(id_node(32, "StdoutLogger")),
9502                            args: vec![],
9503                            type_args: vec![],
9504                        },
9505                    )),
9506                }],
9507                body: Box::new(block(33, vec![], Some(call_inner))),
9508            },
9509        );
9510        let main_fn = node(
9511            40,
9512            NodeKind::FnDecl {
9513                annotations: vec![],
9514                visibility: Visibility::Private,
9515                is_async: false,
9516                name: ident("main"),
9517                generic_params: vec![],
9518                params: vec![],
9519                return_type: None,
9520                effect_clause: vec![],
9521                where_clause: vec![],
9522                body: Box::new(block(41, vec![handling], None)),
9523            },
9524        );
9525
9526        let out = gen(&module(vec![], vec![effect_decl, inner_fn, main_fn]));
9527        // inner() should receive the handler: inner(&__logger)
9528        assert!(
9529            out.contains("inner(&__logger)"),
9530            "handling block should pass handler to effectful call, got: {out}"
9531        );
9532        // The handling block should instantiate the handler. The PascalCase
9533        // identifier is preserved, since it names a type/constructor in Rust.
9534        assert!(
9535            out.contains("let __logger = StdoutLogger()"),
9536            "handling block should instantiate handler, got: {out}"
9537        );
9538    }
9539
9540    #[test]
9541    fn nested_handling_blocks_shadow_handlers() {
9542        use bock_air::AirHandlerPair;
9543
9544        // effect Logger { fn log(msg: String) -> Void }
9545        let effect_decl = node(
9546            1,
9547            NodeKind::EffectDecl {
9548                annotations: vec![],
9549                visibility: Visibility::Public,
9550                name: ident("Logger"),
9551                generic_params: vec![],
9552                components: vec![],
9553                operations: vec![node(
9554                    2,
9555                    NodeKind::FnDecl {
9556                        annotations: vec![],
9557                        visibility: Visibility::Public,
9558                        is_async: false,
9559                        name: ident("log"),
9560                        generic_params: vec![],
9561                        params: vec![typed_param_node(3, "msg", "String")],
9562                        return_type: None,
9563                        effect_clause: vec![],
9564                        where_clause: vec![],
9565                        body: Box::new(block(4, vec![], None)),
9566                    },
9567                )],
9568            },
9569        );
9570
9571        // fn inner() -> String with Logger { ... }
9572        let inner_fn = node(
9573            10,
9574            NodeKind::FnDecl {
9575                annotations: vec![],
9576                visibility: Visibility::Private,
9577                is_async: false,
9578                name: ident("inner"),
9579                generic_params: vec![],
9580                params: vec![],
9581                return_type: None,
9582                effect_clause: vec![type_path(&["Logger"])],
9583                where_clause: vec![],
9584                body: Box::new(block(12, vec![], Some(str_lit(13, "hello")))),
9585            },
9586        );
9587
9588        // Nested handling: inner handling block shadows outer
9589        let inner_call = node(
9590            20,
9591            NodeKind::Call {
9592                callee: Box::new(id_node(21, "inner")),
9593                args: vec![],
9594                type_args: vec![],
9595            },
9596        );
9597        let inner_handling = node(
9598            30,
9599            NodeKind::HandlingBlock {
9600                handlers: vec![AirHandlerPair {
9601                    effect: type_path(&["Logger"]),
9602                    handler: Box::new(id_node(31, "inner_logger")),
9603                }],
9604                body: Box::new(block(32, vec![], Some(inner_call))),
9605            },
9606        );
9607        let outer_handling = node(
9608            40,
9609            NodeKind::HandlingBlock {
9610                handlers: vec![AirHandlerPair {
9611                    effect: type_path(&["Logger"]),
9612                    handler: Box::new(id_node(41, "outer_logger")),
9613                }],
9614                body: Box::new(block(42, vec![inner_handling], None)),
9615            },
9616        );
9617        let main_fn = node(
9618            50,
9619            NodeKind::FnDecl {
9620                annotations: vec![],
9621                visibility: Visibility::Private,
9622                is_async: false,
9623                name: ident("main"),
9624                generic_params: vec![],
9625                params: vec![],
9626                return_type: None,
9627                effect_clause: vec![],
9628                where_clause: vec![],
9629                body: Box::new(block(51, vec![outer_handling], None)),
9630            },
9631        );
9632
9633        let out = gen(&module(vec![], vec![effect_decl, inner_fn, main_fn]));
9634        // Inner handling should shadow: inner(&__logger) where __logger = inner_logger
9635        assert!(
9636            out.contains("let __logger = inner_logger"),
9637            "inner handling should shadow outer handler, got: {out}"
9638        );
9639        assert!(
9640            out.contains("inner(&__logger)"),
9641            "call should use innermost handler, got: {out}"
9642        );
9643    }
9644
9645    // ── Generic impl synthesis (DV12 / P1-b2) ─────────────────────────────────
9646
9647    fn generic_param(id: u32, name: &str) -> GenericParam {
9648        GenericParam {
9649            id,
9650            span: span(),
9651            name: ident(name),
9652            bounds: vec![],
9653        }
9654    }
9655
9656    fn named_type(id: u32, name: &str) -> AIRNode {
9657        node(
9658            id,
9659            NodeKind::TypeNamed {
9660                path: type_path(&[name]),
9661                args: vec![],
9662            },
9663        )
9664    }
9665
9666    /// `record Box[T] { value: T }`.
9667    fn generic_box_record() -> AIRNode {
9668        node(
9669            10,
9670            NodeKind::RecordDecl {
9671                annotations: vec![],
9672                visibility: Visibility::Private,
9673                name: ident("Box"),
9674                generic_params: vec![generic_param(11, "T")],
9675                fields: vec![RecordDeclField {
9676                    id: 12,
9677                    span: span(),
9678                    name: ident("value"),
9679                    ty: TypeExpr::Named {
9680                        id: 13,
9681                        span: span(),
9682                        path: type_path(&["T"]),
9683                        args: vec![],
9684                    },
9685                    default: None,
9686                }],
9687            },
9688        )
9689    }
9690
9691    /// `impl Box { fn get(self) -> T { return self.value } }` — a getter that
9692    /// returns a `self` field by value.
9693    fn generic_box_getter_impl() -> AIRNode {
9694        let self_param = node(
9695            20,
9696            NodeKind::Param {
9697                pattern: Box::new(bind_pat(21, "self")),
9698                ty: None,
9699                default: None,
9700            },
9701        );
9702        let body = block(
9703            22,
9704            vec![],
9705            Some(node(
9706                23,
9707                NodeKind::Return {
9708                    value: Some(Box::new(node(
9709                        24,
9710                        NodeKind::FieldAccess {
9711                            object: Box::new(id_node(25, "self")),
9712                            field: ident("value"),
9713                        },
9714                    ))),
9715                },
9716            )),
9717        );
9718        let method = node(
9719            26,
9720            NodeKind::FnDecl {
9721                annotations: vec![],
9722                visibility: Visibility::Private,
9723                is_async: false,
9724                name: ident("get"),
9725                generic_params: vec![],
9726                params: vec![self_param],
9727                return_type: Some(Box::new(named_type(27, "T"))),
9728                effect_clause: vec![],
9729                where_clause: vec![],
9730                body: Box::new(body),
9731            },
9732        );
9733        node(
9734            30,
9735            NodeKind::ImplBlock {
9736                annotations: vec![],
9737                generic_params: vec![],
9738                trait_path: None,
9739                trait_args: vec![],
9740                target: Box::new(named_type(31, "Box")),
9741                where_clause: vec![],
9742                methods: vec![method],
9743            },
9744        )
9745    }
9746
9747    #[test]
9748    fn generic_impl_synthesizes_impl_and_clone_for_getter() {
9749        // `impl Box { fn get(self) -> T { return self.value } }` for
9750        // `record Box[T]` must synthesize `impl<T: Clone> Box<T>`, derive
9751        // `Clone`, and clone the field read (a `&self` method cannot move a
9752        // non-`Copy` field out).
9753        let out = gen(&module(
9754            vec![],
9755            vec![generic_box_record(), generic_box_getter_impl()],
9756        ));
9757        assert!(
9758            out.contains("#[derive(Clone)]"),
9759            "generic getter target should derive Clone, got: {out}"
9760        );
9761        assert!(
9762            out.contains("impl<T: Clone> Box<T> {"),
9763            "impl should synthesize `<T: Clone>` and apply `Box<T>`, got: {out}"
9764        );
9765        assert!(
9766            out.contains("return self.value.clone();"),
9767            "field return should be cloned, got: {out}"
9768        );
9769    }
9770
9771    #[test]
9772    fn generic_impl_no_clone_bound_when_field_not_returned() {
9773        // A generic impl whose method does NOT return a `self` field by value
9774        // must NOT be over-constrained with a `T: Clone` *impl bound*. (The
9775        // struct itself now always `#[derive(Clone)]`s — GAP-B — but the derive's
9776        // own per-field bound is independent of and does not over-constrain the
9777        // inherent impl.)
9778        let self_param = node(
9779            40,
9780            NodeKind::Param {
9781                pattern: Box::new(bind_pat(41, "self")),
9782                ty: None,
9783                default: None,
9784            },
9785        );
9786        // `fn id_value(self) -> Int { return 0 }` — returns a literal, not a
9787        // `self` field.
9788        let body = block(
9789            42,
9790            vec![],
9791            Some(node(
9792                43,
9793                NodeKind::Return {
9794                    value: Some(Box::new(int_lit(44, "0"))),
9795                },
9796            )),
9797        );
9798        let method = node(
9799            45,
9800            NodeKind::FnDecl {
9801                annotations: vec![],
9802                visibility: Visibility::Private,
9803                is_async: false,
9804                name: ident("zero"),
9805                generic_params: vec![],
9806                params: vec![self_param],
9807                return_type: Some(Box::new(named_type(46, "Int"))),
9808                effect_clause: vec![],
9809                where_clause: vec![],
9810                body: Box::new(body),
9811            },
9812        );
9813        let impl_block = node(
9814            47,
9815            NodeKind::ImplBlock {
9816                annotations: vec![],
9817                generic_params: vec![],
9818                trait_path: None,
9819                trait_args: vec![],
9820                target: Box::new(named_type(48, "Box")),
9821                where_clause: vec![],
9822                methods: vec![method],
9823            },
9824        );
9825        let out = gen(&module(vec![], vec![generic_box_record(), impl_block]));
9826        assert!(
9827            out.contains("impl<T> Box<T> {"),
9828            "impl should synthesize `<T>` (no Clone) for a non-returning method, got: {out}"
9829        );
9830        assert!(
9831            !out.contains("T: Clone"),
9832            "must NOT over-constrain the impl with a `T: Clone` bound, got: {out}"
9833        );
9834    }
9835
9836    #[test]
9837    fn generic_trait_impl_clones_field_wrapped_in_constructor() {
9838        // GAP-B: a generic *trait* impl whose method returns `Some(self.value)`
9839        // moves the field out of `&self`; the body must clone it and the impl
9840        // must carry a `T: Clone` bound — even though the field is wrapped in a
9841        // `Some(...)` constructor (not a bare `return self.value`).
9842        let self_param = node(
9843            60,
9844            NodeKind::Param {
9845                pattern: Box::new(bind_pat(61, "self")),
9846                ty: None,
9847                default: None,
9848            },
9849        );
9850        // `return Some(self.value)`.
9851        let some_call = node(
9852            62,
9853            NodeKind::Call {
9854                callee: Box::new(id_node(63, "Some")),
9855                args: vec![AirArg {
9856                    label: None,
9857                    value: node(
9858                        64,
9859                        NodeKind::FieldAccess {
9860                            object: Box::new(id_node(65, "self")),
9861                            field: ident("value"),
9862                        },
9863                    ),
9864                }],
9865                type_args: vec![],
9866            },
9867        );
9868        let body = block(
9869            66,
9870            vec![],
9871            Some(node(
9872                67,
9873                NodeKind::Return {
9874                    value: Some(Box::new(some_call)),
9875                },
9876            )),
9877        );
9878        let method = node(
9879            68,
9880            NodeKind::FnDecl {
9881                annotations: vec![],
9882                visibility: Visibility::Private,
9883                is_async: false,
9884                name: ident("f"),
9885                generic_params: vec![],
9886                params: vec![self_param],
9887                // `-> Optional[T]`.
9888                return_type: Some(Box::new(node(
9889                    69,
9890                    NodeKind::TypeNamed {
9891                        path: type_path(&["Optional"]),
9892                        args: vec![named_type(70, "T")],
9893                    },
9894                ))),
9895                effect_clause: vec![],
9896                where_clause: vec![],
9897                body: Box::new(body),
9898            },
9899        );
9900        let impl_block = node(
9901            71,
9902            NodeKind::ImplBlock {
9903                annotations: vec![],
9904                generic_params: vec![],
9905                trait_path: Some(type_path(&["P"])),
9906                trait_args: vec![named_type(72, "T")],
9907                target: Box::new(named_type(73, "Box")),
9908                where_clause: vec![],
9909                methods: vec![method],
9910            },
9911        );
9912        let out = gen(&module(vec![], vec![generic_box_record(), impl_block]));
9913        assert!(
9914            out.contains("impl<T: Clone> P<T> for Box<T>"),
9915            "trait impl should synthesize `<T: Clone>` and carry trait args, got: {out}"
9916        );
9917        assert!(
9918            out.contains("self.value.clone()"),
9919            "field wrapped in Some(...) should still be cloned, got: {out}"
9920        );
9921    }
9922
9923    #[test]
9924    fn generic_fn_clones_collection_element_gets_bound() {
9925        // GAP-B (free fn): a generic `fn dup[T](xs: List[T]) -> List[T]` whose
9926        // body lowers a `concat`/`get` with `.cloned()`/`.clone()` needs a
9927        // `T: Clone` bound. We model the `concat` desugar shape (a
9928        // `Call(FieldAccess(xs, "concat"), [xs, xs])`) the checker produces.
9929        let xs_param = typed_param_node(80, "xs", "List");
9930        let recv = id_node(82, "xs");
9931        let concat_call = node(
9932            83,
9933            NodeKind::Call {
9934                callee: Box::new(node(
9935                    84,
9936                    NodeKind::FieldAccess {
9937                        object: Box::new(recv),
9938                        field: ident("concat"),
9939                    },
9940                )),
9941                args: vec![
9942                    AirArg {
9943                        label: None,
9944                        value: id_node(82, "xs"),
9945                    },
9946                    AirArg {
9947                        label: None,
9948                        value: id_node(82, "xs"),
9949                    },
9950                ],
9951                type_args: vec![],
9952            },
9953        );
9954        let body = block(
9955            85,
9956            vec![],
9957            Some(node(
9958                86,
9959                NodeKind::Return {
9960                    value: Some(Box::new(concat_call)),
9961                },
9962            )),
9963        );
9964        let f = node(
9965            87,
9966            NodeKind::FnDecl {
9967                annotations: vec![],
9968                visibility: Visibility::Private,
9969                is_async: false,
9970                name: ident("dup"),
9971                generic_params: vec![generic_param(88, "T")],
9972                params: vec![xs_param],
9973                return_type: Some(Box::new(node(
9974                    89,
9975                    NodeKind::TypeNamed {
9976                        path: type_path(&["List"]),
9977                        args: vec![named_type(90, "T")],
9978                    },
9979                ))),
9980                effect_clause: vec![],
9981                where_clause: vec![],
9982                body: Box::new(body),
9983            },
9984        );
9985        let out = gen(&module(vec![], vec![f]));
9986        assert!(
9987            out.contains("fn dup<T: Clone>"),
9988            "generic fn cloning a collection element should get `T: Clone`, got: {out}"
9989        );
9990    }
9991
9992    #[test]
9993    fn generic_fn_no_clone_bound_without_collection_clone() {
9994        // A generic fn that does NOT clone a collection element must not be
9995        // over-constrained with `Clone`.
9996        let xs_param = typed_param_node(91, "x", "Int");
9997        let body = block(
9998            92,
9999            vec![],
10000            Some(node(
10001                93,
10002                NodeKind::Return {
10003                    value: Some(Box::new(id_node(94, "x"))),
10004                },
10005            )),
10006        );
10007        let f = node(
10008            95,
10009            NodeKind::FnDecl {
10010                annotations: vec![],
10011                visibility: Visibility::Private,
10012                is_async: false,
10013                name: ident("identity"),
10014                generic_params: vec![generic_param(96, "T")],
10015                params: vec![xs_param],
10016                return_type: Some(Box::new(named_type(97, "T"))),
10017                effect_clause: vec![],
10018                where_clause: vec![],
10019                body: Box::new(body),
10020            },
10021        );
10022        let out = gen(&module(vec![], vec![f]));
10023        assert!(
10024            out.contains("fn identity<T>"),
10025            "non-cloning generic fn should keep a bare `<T>`, got: {out}"
10026        );
10027        assert!(
10028            !out.contains("T: Clone"),
10029            "must NOT over-constrain a non-cloning generic fn with Clone, got: {out}"
10030        );
10031    }
10032
10033    #[test]
10034    fn collect_pattern_binding_names_walks_constructor_pat() {
10035        // `Some(x)` binds `x`; the names are collected for the move-reuse scan.
10036        let pat = node(
10037            1,
10038            NodeKind::ConstructorPat {
10039                path: type_path(&["Some"]),
10040                fields: vec![bind_pat(2, "x")],
10041            },
10042        );
10043        let mut names = Vec::new();
10044        RsEmitCtx::collect_pattern_binding_names(&pat, &mut names);
10045        assert_eq!(names, vec!["x".to_string()]);
10046    }
10047
10048    #[test]
10049    fn count_identifier_uses_counts_every_read() {
10050        // A body that reads `x` twice (`pred(x)` then `[x]`) reports 2 uses, so
10051        // the move-reuse analysis flags `x` as needing a clone-on-second-use.
10052        let body = block(
10053            10,
10054            vec![
10055                node(
10056                    11,
10057                    NodeKind::Call {
10058                        callee: Box::new(id_node(12, "pred")),
10059                        args: vec![AirArg {
10060                            label: None,
10061                            value: id_node(13, "x"),
10062                        }],
10063                        type_args: vec![],
10064                    },
10065                ),
10066                node(
10067                    14,
10068                    NodeKind::ListLiteral {
10069                        elems: vec![id_node(15, "x")],
10070                    },
10071                ),
10072            ],
10073            None,
10074        );
10075        assert_eq!(RsEmitCtx::count_identifier_uses(&body, "x"), 2);
10076        assert_eq!(RsEmitCtx::count_identifier_uses(&body, "y"), 0);
10077    }
10078
10079    // ── Per-module native-module tree (S3) ──────────────────────────────────
10080
10081    /// A module node with a declared dotted `path` (e.g. `core.option`), used by
10082    /// the per-module emission tests where the file layout and `mod`/`use`
10083    /// wiring are keyed on the declared module-path.
10084    fn module_with_path(path: &[&str], imports: Vec<AIRNode>, items: Vec<AIRNode>) -> AIRNode {
10085        node(
10086            0,
10087            NodeKind::Module {
10088                path: Some(mod_path(path)),
10089                annotations: vec![],
10090                imports,
10091                items,
10092            },
10093        )
10094    }
10095
10096    /// An `import <path>.{ name }` AIR node (a single-item `Named` import).
10097    fn import_named(id: u32, path: &[&str], name: &str) -> AIRNode {
10098        node(
10099            id,
10100            NodeKind::ImportDecl {
10101                path: mod_path(path),
10102                items: ImportItems::Named(vec![imported_name(name)]),
10103            },
10104        )
10105    }
10106
10107    /// A bare `fn <name>() -> <tail>` declaration with the given visibility.
10108    fn fn_decl_tail(id: u32, vis: Visibility, name: &str, tail: AIRNode) -> AIRNode {
10109        node(
10110            id,
10111            NodeKind::FnDecl {
10112                annotations: vec![],
10113                visibility: vis,
10114                is_async: false,
10115                name: ident(name),
10116                generic_params: vec![],
10117                params: vec![],
10118                return_type: None,
10119                effect_clause: vec![],
10120                where_clause: vec![],
10121                body: Box::new(block(id + 1, vec![], Some(tail))),
10122            },
10123        )
10124    }
10125
10126    #[test]
10127    fn per_module_emits_native_rust_module_tree() {
10128        // entry `module main` uses `mathutil.add_one`; `module mathutil` exports
10129        // a `public fn add_one`. Per-module emission must produce the native
10130        // module *source* tree: `src/main.rs` (with `mod mathutil;` + `use
10131        // crate::mathutil::{add_one};`), and `src/mathutil.rs` — a real module
10132        // tree, not a single collapsed file. The `Cargo.toml` run affordance is
10133        // emitted by the scaffolder (project mode), NOT by codegen (S6a / DV18).
10134        let call = node(
10135            10,
10136            NodeKind::Call {
10137                callee: Box::new(id_node(11, "add_one")),
10138                args: vec![AirArg {
10139                    label: None,
10140                    value: int_lit(12, "6"),
10141                }],
10142                type_args: vec![],
10143            },
10144        );
10145        let main_mod = module_with_path(
10146            &["main"],
10147            vec![import_named(5, &["mathutil"], "add_one")],
10148            vec![fn_decl_tail(1, Visibility::Private, "main", call)],
10149        );
10150        let util_mod = module_with_path(
10151            &["mathutil"],
10152            vec![],
10153            vec![fn_decl_tail(
10154                20,
10155                Visibility::Public,
10156                "add_one",
10157                int_lit(22, "7"),
10158            )],
10159        );
10160
10161        let gen = RsGenerator::new();
10162        let out = gen
10163            .generate_project(&[
10164                (&main_mod, std::path::Path::new("src/main.bock")),
10165                (&util_mod, std::path::Path::new("src/mathutil.bock")),
10166            ])
10167            .unwrap();
10168
10169        let by_name = |p: &str| out.files.iter().find(|f| f.path == std::path::Path::new(p));
10170        let main_file = by_name("src/main.rs").expect("src/main.rs emitted");
10171        let util_file = by_name("src/mathutil.rs").expect("src/mathutil.rs emitted");
10172        // Codegen no longer emits the manifest (S6a / DV18) — the scaffolder
10173        // owns the `Cargo.toml` in project mode.
10174        assert!(
10175            by_name("Cargo.toml").is_none(),
10176            "codegen must NOT emit Cargo.toml — the scaffolder owns it (S6a)"
10177        );
10178
10179        assert!(
10180            main_file.content.contains("mod mathutil;"),
10181            "main.rs must declare the sibling module; got:\n{}",
10182            main_file.content
10183        );
10184        assert!(
10185            main_file
10186                .content
10187                .contains("use crate::mathutil::{add_one};"),
10188            "main.rs must `use` the cross-module fn; got:\n{}",
10189            main_file.content
10190        );
10191        // The inner attribute must precede the `mod` declarations.
10192        let attr = main_file.content.find("#![allow").expect("inner attr");
10193        let modline = main_file.content.find("mod mathutil;").unwrap();
10194        assert!(attr < modline, "inner attribute must precede `mod`");
10195        assert!(
10196            util_file.content.contains("pub fn add_one("),
10197            "mathutil.rs must carry the exported fn; got:\n{}",
10198            util_file.content
10199        );
10200    }
10201
10202    #[test]
10203    fn per_module_builds_nested_mod_tree_wiring() {
10204        // entry uses `core.option.get_or`. The nested `core.option` module must
10205        // produce `src/core/option.rs` (the leaf), a `src/core.rs` wiring file
10206        // declaring `pub mod option;`, and `mod core;` at the crate root.
10207        let call = node(
10208            10,
10209            NodeKind::Call {
10210                callee: Box::new(id_node(11, "get_or")),
10211                args: vec![],
10212                type_args: vec![],
10213            },
10214        );
10215        let main_mod = module_with_path(
10216            &["main"],
10217            vec![import_named(5, &["core", "option"], "get_or")],
10218            vec![fn_decl_tail(1, Visibility::Private, "main", call)],
10219        );
10220        let opt_mod = module_with_path(
10221            &["core", "option"],
10222            vec![],
10223            vec![fn_decl_tail(
10224                20,
10225                Visibility::Public,
10226                "get_or",
10227                int_lit(22, "0"),
10228            )],
10229        );
10230
10231        let gen = RsGenerator::new();
10232        let out = gen
10233            .generate_project(&[
10234                (&main_mod, std::path::Path::new("src/main.bock")),
10235                (&opt_mod, std::path::Path::new("src/core/option.bock")),
10236            ])
10237            .unwrap();
10238        let by_name = |p: &str| out.files.iter().find(|f| f.path == std::path::Path::new(p));
10239        by_name("src/core/option.rs").expect("nested leaf module file emitted");
10240        let wiring = by_name("src/core.rs").expect("namespace wiring file emitted");
10241        assert!(
10242            wiring.content.contains("pub mod option;"),
10243            "src/core.rs must declare `pub mod option;`; got:\n{}",
10244            wiring.content
10245        );
10246        let main_file = by_name("src/main.rs").expect("src/main.rs emitted");
10247        assert!(
10248            main_file.content.contains("mod core;"),
10249            "main.rs must declare `mod core;`; got:\n{}",
10250            main_file.content
10251        );
10252        assert!(
10253            main_file
10254                .content
10255                .contains("use crate::core::option::{get_or};"),
10256            "main.rs must `use crate::core::option::{{get_or}};`; got:\n{}",
10257            main_file.content
10258        );
10259    }
10260
10261    /// `fn f() { let x = if (c) { 1 } else { return 0 }  x }` — value-position
10262    /// `if` with a diverging else. The shared value-CF hoist lowers it to a
10263    /// deferred-init `let mut __bock_cf_0;` plus statement-form assignment, never
10264    /// `/* unsupported */`.
10265    fn diverging_value_if_fn() -> AIRNode {
10266        let then_b = block(2, vec![], Some(int_lit(3, "1")));
10267        let ret = node(
10268            5,
10269            NodeKind::Return {
10270                value: Some(Box::new(int_lit(6, "0"))),
10271            },
10272        );
10273        let else_b = block(4, vec![], Some(ret));
10274        let if_node = node(
10275            1,
10276            NodeKind::If {
10277                let_pattern: None,
10278                condition: Box::new(id_node(7, "c")),
10279                then_block: Box::new(then_b),
10280                else_block: Some(Box::new(else_b)),
10281            },
10282        );
10283        let let_x = node(
10284            10,
10285            NodeKind::LetBinding {
10286                is_mut: false,
10287                pattern: Box::new(bind_pat(11, "x")),
10288                ty: None,
10289                value: Box::new(if_node),
10290            },
10291        );
10292        let body = block(20, vec![let_x], Some(id_node(21, "x")));
10293        let f = node(
10294            30,
10295            NodeKind::FnDecl {
10296                annotations: vec![],
10297                visibility: Visibility::Private,
10298                is_async: false,
10299                name: ident("f"),
10300                generic_params: vec![],
10301                params: vec![],
10302                return_type: None,
10303                effect_clause: vec![],
10304                where_clause: vec![],
10305                body: Box::new(body),
10306            },
10307        );
10308        module(vec![], vec![f])
10309    }
10310
10311    #[test]
10312    fn diverging_value_if_hoists_to_stmt_form_no_unsupported() {
10313        let out = gen(&diverging_value_if_fn());
10314        assert!(
10315            !out.contains("/* unsupported */"),
10316            "diverging value-if must not emit `/* unsupported */`, got: {out}"
10317        );
10318        assert!(
10319            out.contains("let mut __bock_cf_0"),
10320            "must declare a deferred-init temp, got: {out}"
10321        );
10322        assert!(
10323            out.contains("__bock_cf_0 = 1"),
10324            "value arm must assign the temp, got: {out}"
10325        );
10326        assert!(
10327            out.contains("return Err") || out.contains("return 0"),
10328            "diverging arm must keep its return, got: {out}"
10329        );
10330    }
10331
10332    // ── Rust-specific example-hardening regression tests ─────────────────────
10333
10334    fn float_lit(id: u32, val: &str) -> AIRNode {
10335        node(
10336            id,
10337            NodeKind::Literal {
10338                lit: Literal::Float(val.into()),
10339            },
10340        )
10341    }
10342
10343    fn pow(id: u32, left: AIRNode, right: AIRNode) -> AIRNode {
10344        node(
10345            id,
10346            NodeKind::BinaryOp {
10347                op: BinOp::Pow,
10348                left: Box::new(left),
10349                right: Box::new(right),
10350            },
10351        )
10352    }
10353
10354    fn type_named_node(id: u32, name: &str) -> AIRNode {
10355        node(
10356            id,
10357            NodeKind::TypeNamed {
10358                path: type_path(&[name]),
10359                args: vec![],
10360            },
10361        )
10362    }
10363
10364    /// `let c: Char = 'A'` annotates the binding `char`, not the unknown `Char`
10365    /// (E0425). The example `type-zoo` exercises this.
10366    #[test]
10367    fn rust_char_type_annotation_lowers_to_char() {
10368        let ty = type_named_node(3, "Char");
10369        let let_node = node(
10370            1,
10371            NodeKind::LetBinding {
10372                pattern: Box::new(bind_pat(2, "c")),
10373                value: Box::new(str_lit(4, "A")),
10374                ty: Some(Box::new(ty)),
10375                is_mut: false,
10376            },
10377        );
10378        let mut ctx = RsEmitCtx::new();
10379        ctx.emit_node(&let_node).unwrap();
10380        assert!(ctx.buf.contains("let c: char ="), "got: {}", ctx.buf);
10381        assert!(!ctx.buf.contains("Char"), "got: {}", ctx.buf);
10382    }
10383
10384    /// `2 ** 10` lowers to `i64::pow` with a `u32`-cast exponent — `pow` takes
10385    /// `u32`, so emitting `10_i64` is E0308. The `type-zoo` `power` case.
10386    #[test]
10387    fn rust_int_pow_casts_exponent_to_u32() {
10388        let expr = pow(1, int_lit(2, "2"), int_lit(3, "10"));
10389        let mut ctx = RsEmitCtx::new();
10390        ctx.emit_expr(&expr).unwrap();
10391        assert_eq!(ctx.buf, "(2_i64).pow((10_i64) as u32)", "got: {}", ctx.buf);
10392    }
10393
10394    /// `b ** 3.0` (a Float-literal operand) lowers to `f64::powf` — `f64` has no
10395    /// `pow`. The exponent is cast `as f64` to admit an integer-literal exponent.
10396    #[test]
10397    fn rust_float_pow_uses_powf() {
10398        let expr = pow(1, id_node(2, "b"), float_lit(3, "3.0"));
10399        let mut ctx = RsEmitCtx::new();
10400        ctx.emit_expr(&expr).unwrap();
10401        assert_eq!(ctx.buf, "(b).powf((3.0_f64) as f64)", "got: {}", ctx.buf);
10402    }
10403
10404    /// A function declared to return `Fn(Int) -> Int` lowers its return type to
10405    /// `impl Fn(i64) -> i64`, its `Fn`-typed params to `impl Fn(..) + 'static`,
10406    /// and its tail closure to `move |..|` — a capturing closure cannot coerce
10407    /// to a `fn` pointer (E0308) and the returned `impl Fn` outlives the frame
10408    /// (E0373/E0310). The `type-zoo` `compose_int` shape.
10409    #[test]
10410    fn rust_closure_returning_fn_uses_impl_fn_and_move() {
10411        let fn_ty = |id: u32| {
10412            node(
10413                id,
10414                NodeKind::TypeFunction {
10415                    params: vec![type_named_node(id + 1, "Int")],
10416                    ret: Box::new(type_named_node(id + 2, "Int")),
10417                    effects: vec![],
10418                },
10419            )
10420        };
10421        // body: `(x) => f(g(x))`
10422        let inner_call = node(
10423            30,
10424            NodeKind::Call {
10425                callee: Box::new(id_node(31, "g")),
10426                args: vec![AirArg {
10427                    label: None,
10428                    value: id_node(32, "x"),
10429                }],
10430                type_args: vec![],
10431            },
10432        );
10433        let outer_call = node(
10434            33,
10435            NodeKind::Call {
10436                callee: Box::new(id_node(34, "f")),
10437                args: vec![AirArg {
10438                    label: None,
10439                    value: inner_call,
10440                }],
10441                type_args: vec![],
10442            },
10443        );
10444        let lambda = node(
10445            35,
10446            NodeKind::Lambda {
10447                params: vec![node(
10448                    36,
10449                    NodeKind::Param {
10450                        pattern: Box::new(bind_pat(37, "x")),
10451                        ty: None,
10452                        default: None,
10453                    },
10454                )],
10455                body: Box::new(outer_call),
10456            },
10457        );
10458        let f = node(
10459            1,
10460            NodeKind::FnDecl {
10461                annotations: vec![],
10462                visibility: Visibility::Public,
10463                is_async: false,
10464                name: ident("compose_int"),
10465                generic_params: vec![],
10466                params: vec![
10467                    node(
10468                        2,
10469                        NodeKind::Param {
10470                            pattern: Box::new(bind_pat(3, "f")),
10471                            ty: Some(Box::new(fn_ty(10))),
10472                            default: None,
10473                        },
10474                    ),
10475                    node(
10476                        4,
10477                        NodeKind::Param {
10478                            pattern: Box::new(bind_pat(5, "g")),
10479                            ty: Some(Box::new(fn_ty(15))),
10480                            default: None,
10481                        },
10482                    ),
10483                ],
10484                return_type: Some(Box::new(fn_ty(20))),
10485                effect_clause: vec![],
10486                where_clause: vec![],
10487                body: Box::new(block(40, vec![], Some(lambda))),
10488            },
10489        );
10490        let out = gen(&module(vec![], vec![f]));
10491        assert!(
10492            out.contains("-> impl Fn(i64) -> i64"),
10493            "return must be impl Fn, got: {out}"
10494        );
10495        assert!(
10496            out.contains("f: impl Fn(i64) -> i64 + 'static"),
10497            "params must carry + 'static, got: {out}"
10498        );
10499        assert!(
10500            out.contains("move |x"),
10501            "tail closure must move, got: {out}"
10502        );
10503        assert!(check_rs_syntax(&out), "generated rust must parse: {out}");
10504    }
10505
10506    /// A non-`Copy` param read *inside a loop* (`for e in xs { is_cat(e, cat) }`)
10507    /// is moved on the first iteration, so each by-value pass must clone — even
10508    /// though the param appears only once textually. The `expense-tracker`
10509    /// `category_total` shape (param `cat`).
10510    #[test]
10511    fn rust_param_used_in_loop_is_cloned() {
10512        // for e in expenses { other(cat) }
10513        let call = node(
10514            10,
10515            NodeKind::Call {
10516                callee: Box::new(id_node(11, "other")),
10517                args: vec![AirArg {
10518                    label: None,
10519                    value: id_node(12, "cat"),
10520                }],
10521                type_args: vec![],
10522            },
10523        );
10524        let for_node = node(
10525            13,
10526            NodeKind::For {
10527                pattern: Box::new(bind_pat(14, "e")),
10528                iterable: Box::new(id_node(15, "expenses")),
10529                body: Box::new(block(16, vec![call], None)),
10530            },
10531        );
10532        let f = node(
10533            1,
10534            NodeKind::FnDecl {
10535                annotations: vec![],
10536                visibility: Visibility::Public,
10537                is_async: false,
10538                name: ident("category_total"),
10539                generic_params: vec![],
10540                params: vec![
10541                    typed_param_node(2, "expenses", "Expenses"),
10542                    typed_param_node(3, "cat", "Category"),
10543                ],
10544                return_type: None,
10545                effect_clause: vec![],
10546                where_clause: vec![],
10547                body: Box::new(block(5, vec![for_node], None)),
10548            },
10549        );
10550        let out = gen(&module(vec![], vec![f]));
10551        assert!(out.contains("other(cat.clone())"), "got: {out}");
10552    }
10553
10554    /// A `for` loop variable passed by value to a call and *then* read again
10555    /// (`is_cat(e, cat)` then `e.amount`) must clone the by-value pass — the
10556    /// first consumer moves the element (E0382). The `expense-tracker` `e` shape.
10557    #[test]
10558    fn rust_loop_var_passed_then_read_is_cloned() {
10559        let call = node(
10560            10,
10561            NodeKind::Call {
10562                callee: Box::new(id_node(11, "consume")),
10563                args: vec![AirArg {
10564                    label: None,
10565                    value: id_node(12, "e"),
10566                }],
10567                type_args: vec![],
10568            },
10569        );
10570        let read = node(
10571            17,
10572            NodeKind::FieldAccess {
10573                object: Box::new(id_node(18, "e")),
10574                field: ident("amount"),
10575            },
10576        );
10577        let for_node = node(
10578            13,
10579            NodeKind::For {
10580                pattern: Box::new(bind_pat(14, "e")),
10581                iterable: Box::new(id_node(15, "items")),
10582                body: Box::new(block(16, vec![call], Some(read))),
10583            },
10584        );
10585        let mut ctx = RsEmitCtx::new();
10586        ctx.emit_node(&for_node).unwrap();
10587        assert!(ctx.buf.contains("consume(e.clone())"), "got: {}", ctx.buf);
10588    }
10589
10590    /// Iterating a *field access* of a binding reused after the loop
10591    /// (`for row in dataset.rows` then `dataset.clone()`) clones the iterable so
10592    /// the owner is not partially moved (E0382). The `ml-data-prep` shape.
10593    #[test]
10594    fn rust_for_over_reused_field_clones_iterable() {
10595        // let dataset = ...; for row in dataset.rows { } ; use(dataset)
10596        let for_node = node(
10597            13,
10598            NodeKind::For {
10599                pattern: Box::new(bind_pat(14, "row")),
10600                iterable: Box::new(node(
10601                    15,
10602                    NodeKind::FieldAccess {
10603                        object: Box::new(id_node(16, "dataset")),
10604                        field: ident("rows"),
10605                    },
10606                )),
10607                body: Box::new(block(17, vec![], None)),
10608            },
10609        );
10610        let use_call = node(
10611            20,
10612            NodeKind::Call {
10613                callee: Box::new(id_node(21, "use_it")),
10614                args: vec![AirArg {
10615                    label: None,
10616                    value: id_node(22, "dataset"),
10617                }],
10618                type_args: vec![],
10619            },
10620        );
10621        let let_dataset = node(
10622            1,
10623            NodeKind::LetBinding {
10624                pattern: Box::new(bind_pat(2, "dataset")),
10625                value: Box::new(int_lit(3, "0")),
10626                ty: None,
10627                is_mut: false,
10628            },
10629        );
10630        let body = block(40, vec![let_dataset, for_node, use_call], None);
10631        let f = fn_decl_tail_with_body(50, "main", body);
10632        let out = gen(&module(vec![], vec![f]));
10633        assert!(
10634            out.contains("for row in dataset.rows.clone()"),
10635            "got: {out}"
10636        );
10637    }
10638
10639    fn fn_decl_tail_with_body(id: u32, name: &str, body: AIRNode) -> AIRNode {
10640        node(
10641            id,
10642            NodeKind::FnDecl {
10643                annotations: vec![],
10644                visibility: Visibility::Private,
10645                is_async: false,
10646                name: ident(name),
10647                generic_params: vec![],
10648                params: vec![],
10649                return_type: None,
10650                effect_clause: vec![],
10651                where_clause: vec![],
10652                body: Box::new(body),
10653            },
10654        )
10655    }
10656
10657    /// A function whose declared return type is a `Fn`-typed **`type` alias**
10658    /// (`type EventHandler = Fn() -> Void`; `fn with_logging(..) -> EventHandler`)
10659    /// is recognised as a closure-returning function exactly as a literal `Fn(..)`
10660    /// return would be: the return + the `Fn`-typed param lower to `impl Fn(..)`,
10661    /// and the tail closure gets `move`. A bare `fn` pointer (the alias's own
10662    /// lowering) would reject the body's *capturing* closure (E0308). The
10663    /// `react-components` `with_logging` shape.
10664    #[test]
10665    fn rust_fn_typed_alias_return_uses_impl_fn_and_move() {
10666        // type EventHandler = Fn() -> Void
10667        let alias = node(
10668            1,
10669            NodeKind::TypeAlias {
10670                annotations: vec![],
10671                visibility: Visibility::Private,
10672                name: ident("EventHandler"),
10673                generic_params: vec![],
10674                ty: Box::new(node(
10675                    2,
10676                    NodeKind::TypeFunction {
10677                        params: vec![],
10678                        ret: Box::new(type_named_node(3, "Void")),
10679                        effects: vec![],
10680                    },
10681                )),
10682                where_clause: vec![],
10683            },
10684        );
10685        // fn with_logging(handler: EventHandler) -> EventHandler { () => handler() }
10686        let inner_call = node(
10687            30,
10688            NodeKind::Call {
10689                callee: Box::new(id_node(31, "handler")),
10690                args: vec![],
10691                type_args: vec![],
10692            },
10693        );
10694        let lambda = node(
10695            32,
10696            NodeKind::Lambda {
10697                params: vec![],
10698                body: Box::new(inner_call),
10699            },
10700        );
10701        let handler_param = node(
10702            10,
10703            NodeKind::Param {
10704                pattern: Box::new(bind_pat(11, "handler")),
10705                ty: Some(Box::new(type_named_node(12, "EventHandler"))),
10706                default: None,
10707            },
10708        );
10709        let f = node(
10710            20,
10711            NodeKind::FnDecl {
10712                annotations: vec![],
10713                visibility: Visibility::Private,
10714                is_async: false,
10715                name: ident("with_logging"),
10716                generic_params: vec![],
10717                params: vec![handler_param],
10718                return_type: Some(Box::new(type_named_node(21, "EventHandler"))),
10719                effect_clause: vec![],
10720                where_clause: vec![],
10721                body: Box::new(block(40, vec![], Some(lambda))),
10722            },
10723        );
10724        let out = gen(&module(vec![], vec![alias, f]));
10725        assert!(
10726            out.contains("-> impl Fn() -> ()"),
10727            "alias return must lower to impl Fn, got: {out}"
10728        );
10729        assert!(
10730            out.contains("handler: impl Fn() -> () + 'static"),
10731            "alias param must lower to impl Fn + 'static, got: {out}"
10732        );
10733        assert!(
10734            out.contains("move ||"),
10735            "tail closure must move-capture, got: {out}"
10736        );
10737        // The alias declaration itself keeps the `fn`-pointer form (the only
10738        // `Fn`-shaped type nameable in a Rust `type` alias).
10739        assert!(
10740            out.contains("type EventHandler = fn() -> ();"),
10741            "alias decl must stay a fn pointer, got: {out}"
10742        );
10743        assert!(check_rs_syntax(&out), "generated rust must parse: {out}");
10744    }
10745
10746    /// A non-`Copy` parameter used as the bare **value** of several sibling
10747    /// `if`/`else` arms (`let a = if (..) { value } else { .. }; let b = if (..)
10748    /// { value } else { .. }`) is moved by the first arm-tail; the later arm is a
10749    /// use-after-move (E0382). The Rust backend clones the reused-param tail
10750    /// (`{ value.clone() }`). The `react-components` `update_field` shape.
10751    #[test]
10752    fn rust_reused_param_value_tail_is_cloned() {
10753        // let a = if (c) { value } else { "" }
10754        // let b = if (c) { value } else { "" }
10755        let make_if = |id: u32| {
10756            node(
10757                id,
10758                NodeKind::If {
10759                    let_pattern: None,
10760                    condition: Box::new(id_node(id + 1, "c")),
10761                    then_block: Box::new(block(id + 2, vec![], Some(id_node(id + 3, "value")))),
10762                    else_block: Some(Box::new(block(id + 4, vec![], Some(str_lit(id + 5, ""))))),
10763                },
10764            )
10765        };
10766        let let_a = node(
10767            50,
10768            NodeKind::LetBinding {
10769                is_mut: false,
10770                pattern: Box::new(bind_pat(51, "a")),
10771                ty: None,
10772                value: Box::new(make_if(60)),
10773            },
10774        );
10775        let let_b = node(
10776            70,
10777            NodeKind::LetBinding {
10778                is_mut: false,
10779                pattern: Box::new(bind_pat(71, "b")),
10780                ty: None,
10781                value: Box::new(make_if(80)),
10782            },
10783        );
10784        let value_param = node(
10785            10,
10786            NodeKind::Param {
10787                pattern: Box::new(bind_pat(11, "value")),
10788                ty: Some(Box::new(type_named_node(12, "String"))),
10789                default: None,
10790            },
10791        );
10792        let f = node(
10793            1,
10794            NodeKind::FnDecl {
10795                annotations: vec![],
10796                visibility: Visibility::Private,
10797                is_async: false,
10798                name: ident("pick"),
10799                generic_params: vec![],
10800                params: vec![value_param],
10801                return_type: None,
10802                effect_clause: vec![],
10803                where_clause: vec![],
10804                body: Box::new(block(40, vec![let_a, let_b], None)),
10805            },
10806        );
10807        let out = gen(&module(vec![], vec![f]));
10808        assert!(
10809            out.contains("{ value.clone() }"),
10810            "reused String param tail must clone, got: {out}"
10811        );
10812    }
10813
10814    /// A **generic** `T`-typed parameter used as a bare arm-tail value
10815    /// (`max_of<T: Ord>(a, b) { if (a > b) { a } else { b } }`) must NOT be
10816    /// cloned: `T` carries no `Clone` bound, so `a.clone()` is E0599 — and the
10817    /// arm-tail is the value's last use, so no clone is needed. Guards the
10818    /// reused-param tail clone against the `type-zoo` `max_of` regression.
10819    #[test]
10820    fn rust_generic_param_value_tail_not_cloned() {
10821        let if_node = node(
10822            30,
10823            NodeKind::If {
10824                let_pattern: None,
10825                condition: Box::new(node(
10826                    31,
10827                    NodeKind::BinaryOp {
10828                        op: BinOp::Gt,
10829                        left: Box::new(id_node(32, "a")),
10830                        right: Box::new(id_node(33, "b")),
10831                    },
10832                )),
10833                then_block: Box::new(block(34, vec![], Some(id_node(35, "a")))),
10834                else_block: Some(Box::new(block(36, vec![], Some(id_node(37, "b"))))),
10835            },
10836        );
10837        let f = node(
10838            1,
10839            NodeKind::FnDecl {
10840                annotations: vec![],
10841                visibility: Visibility::Private,
10842                is_async: false,
10843                name: ident("max_of"),
10844                generic_params: vec![generic_param(2, "T")],
10845                params: vec![typed_param_node(3, "a", "T"), typed_param_node(4, "b", "T")],
10846                return_type: Some(Box::new(type_named_node(5, "T"))),
10847                effect_clause: vec![],
10848                where_clause: vec![],
10849                body: Box::new(block(40, vec![], Some(if_node))),
10850            },
10851        );
10852        let out = gen(&module(vec![], vec![f]));
10853        assert!(
10854            !out.contains("a.clone()") && !out.contains("b.clone()"),
10855            "generic T param tail must NOT clone, got: {out}"
10856        );
10857        assert!(
10858            out.contains("{ a } else { b }"),
10859            "generic arm tails must be emitted bare, got: {out}"
10860        );
10861    }
10862
10863    /// A collection-typed binding interpolated into a string formats with `{:?}`
10864    /// — a `Vec`/`HashMap`/`HashSet` has no `Display` (E0277). The `type-zoo`
10865    /// `${keys}` (from `map.keys()`) shape.
10866    #[test]
10867    fn rust_interpolated_collection_binding_uses_debug_fmt() {
10868        // let keys = map.keys(); println("k=${keys}")
10869        let keys_call = node(
10870            10,
10871            NodeKind::MethodCall {
10872                receiver: Box::new(id_node(11, "map")),
10873                method: ident("keys"),
10874                args: vec![],
10875                type_args: vec![],
10876            },
10877        );
10878        let let_keys = node(
10879            1,
10880            NodeKind::LetBinding {
10881                pattern: Box::new(bind_pat(2, "keys")),
10882                value: Box::new(keys_call),
10883                ty: None,
10884                is_mut: false,
10885            },
10886        );
10887        let interp = node(
10888            20,
10889            NodeKind::Interpolation {
10890                parts: vec![
10891                    AirInterpolationPart::Literal("k=".to_string()),
10892                    AirInterpolationPart::Expr(Box::new(id_node(21, "keys"))),
10893                ],
10894            },
10895        );
10896        let print = node(
10897            22,
10898            NodeKind::Call {
10899                callee: Box::new(id_node(23, "println")),
10900                args: vec![AirArg {
10901                    label: None,
10902                    value: interp,
10903                }],
10904                type_args: vec![],
10905            },
10906        );
10907        let body = block(30, vec![let_keys, print], None);
10908        let f = fn_decl_tail_with_body(40, "main", body);
10909        let out = gen(&module(vec![], vec![f]));
10910        assert!(
10911            out.contains("\"k={:?}\""),
10912            "interpolated collection must use {{:?}}, got: {out}"
10913        );
10914    }
10915}