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()), ¶ms[1..])
3894 }
3895 _ if is_assoc => (None, ¶ms[..]),
3896 _ => (Some("&self".to_string()), ¶ms[..]),
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()), ¶ms[1..])
3979 }
3980 _ if is_assoc => (None, ¶ms[..]),
3981 _ => (Some("&self".to_string()), ¶ms[..]),
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 ¶m.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}