Skip to main content

normalize_refactor/
extract_function.rs

1//! Extract-function recipe: lift a region of code into a new function.
2//!
3//! Uses CFG liveness data from the index to determine the correct parameter
4//! list and return type for the extracted function.
5//!
6//! # Algorithm
7//! 1. Query `cfg_blocks` to find all blocks whose line range overlaps the
8//!    selected region.
9//! 2. Run a backward-dataflow liveness pass over the *whole function* to
10//!    compute `live_in[B]` / `live_out[B]` for every block.
11//! 3. Derive:
12//!    - **parameters** = ∪(live_in[B] for B in R) ∩ vars_defined_outside_R
13//!    - **return vars** = ∪(live_out[B] for B in exit_blocks(R)) ∩ vars_defined_in_R
14//! 4. Inspect `cfg_effects` in the region to detect `async`/generator/defer/acquire.
15//! 5. Inspect `cfg_edges` for `EdgeKind::Exception` edges that leave the region.
16//! 6. Generate language-appropriate source text and produce a `RefactoringPlan`.
17
18use std::collections::{BTreeSet, HashMap, HashSet};
19use std::path::Path;
20
21use crate::{PlannedEdit, RefactoringPlan};
22
23// ─── Public data model ────────────────────────────────────────────────────────
24
25/// Outcome of a successful extract-function plan.
26pub struct ExtractFunctionOutcome {
27    /// The generated `RefactoringPlan` (new function definition + updated call site).
28    pub plan: RefactoringPlan,
29    /// User-provided name for the new function.
30    pub function_name: String,
31    /// Parameters inferred from liveness analysis.
32    pub parameters: Vec<Parameter>,
33    /// Return type inferred from liveness analysis.
34    pub return_type: ReturnType,
35    /// Whether the extracted function must be `async`.
36    pub is_async: bool,
37    /// Whether the extracted function is a generator.
38    pub is_generator: bool,
39    /// Line number in the original file where the call site will appear
40    /// (first line of the extracted region).
41    pub call_site_line: u32,
42}
43
44/// A parameter of the extracted function.
45#[derive(Debug, Clone)]
46pub struct Parameter {
47    pub name: String,
48    /// Type annotation, if discoverable from the source. `None` means unknown.
49    pub inferred_type: Option<String>,
50    /// Rust: whether the binding in the original function is `mut`.
51    pub mutable: bool,
52}
53
54/// Return type of the extracted function.
55#[derive(Debug, Clone)]
56pub enum ReturnType {
57    Unit,
58    Single(String),
59    Tuple(Vec<String>),
60    /// Rust-style `Result<T, E>` when exception edges escape the boundary.
61    Result(String, String),
62}
63
64/// A warning emitted when the extraction may produce semantically surprising code.
65#[derive(Debug, Clone)]
66pub enum ExtractionWarning {
67    DeferCrossedBoundary {
68        line: u32,
69    },
70    ResourceLifetimeCrossedBoundary {
71        label: String,
72        acquire_line: u32,
73    },
74    ExceptionEscapesBoundary {
75        exception_type: String,
76        throw_line: u32,
77    },
78    MultipleLiveOutVariables {
79        names: Vec<String>,
80    },
81}
82
83impl ExtractionWarning {
84    pub fn to_string_lossy(&self) -> String {
85        match self {
86            ExtractionWarning::DeferCrossedBoundary { line } => {
87                format!(
88                    "defer/deferred statement at line {} crosses extraction boundary; defer semantics may not transfer correctly",
89                    line
90                )
91            }
92            ExtractionWarning::ResourceLifetimeCrossedBoundary {
93                label,
94                acquire_line,
95            } => {
96                format!(
97                    "resource '{}' acquired at line {} but not released within extracted region; resource lifetime crosses extraction boundary",
98                    label, acquire_line
99                )
100            }
101            ExtractionWarning::ExceptionEscapesBoundary {
102                exception_type,
103                throw_line,
104            } => {
105                format!(
106                    "exception '{}' thrown at line {} escapes the extraction boundary; the extracted function must declare or handle it",
107                    exception_type, throw_line
108                )
109            }
110            ExtractionWarning::MultipleLiveOutVariables { names } => {
111                format!(
112                    "multiple live-out variables ({}); language may not support multiple return values — consider returning a struct",
113                    names.join(", ")
114                )
115            }
116        }
117    }
118}
119
120// ─── Internal intermediate structs (from index) ───────────────────────────────
121
122#[derive(Debug)]
123struct BlockRow {
124    block_id: u32,
125    #[allow(dead_code)]
126    kind: String,
127    start_line: u32,
128    end_line: u32,
129}
130
131#[derive(Debug)]
132struct EdgeRow {
133    from: u32,
134    to: u32,
135    kind: String,
136    exception_type: Option<String>,
137}
138
139#[derive(Debug)]
140struct EffectRow {
141    block_id: u32,
142    kind: String,
143    line: u32,
144    label: Option<String>,
145}
146
147// ─── Entry point ──────────────────────────────────────────────────────────────
148
149/// Build an extract-function plan without touching the filesystem.
150///
151/// `file_abs` is the absolute path to the file.
152/// `content` is the file's current text.
153/// `start_line` and `end_line` are **1-based, inclusive** line numbers selecting
154/// the region to extract.
155/// `function_name` is the user-provided name for the new function.
156pub async fn plan_extract_function(
157    ctx: &crate::RefactoringContext,
158    file_abs: &Path,
159    content: &str,
160    start_line: u32,
161    end_line: u32,
162    function_name: &str,
163) -> Result<ExtractFunctionOutcome, String> {
164    // ── 0. Resolve the file relative to the project root for index queries ──
165    let rel_path = file_abs
166        .strip_prefix(&ctx.root)
167        .unwrap_or(file_abs)
168        .to_string_lossy()
169        .to_string();
170
171    // ── 1. Detect grammar / language ──────────────────────────────────────────
172    let grammar_name = normalize_languages::support_for_path(file_abs)
173        .map(|s| s.name().to_string())
174        .unwrap_or_default();
175
176    // ── 2. Query the index ────────────────────────────────────────────────────
177    let index = ctx.index.as_ref().ok_or_else(|| {
178        "extract-function requires the facts index — run `normalize structure rebuild` first"
179            .to_string()
180    })?;
181
182    let conn = index.connection();
183
184    // Find the enclosing function: its name and start line.
185    let func_row = find_enclosing_function(conn, &rel_path, start_line, end_line).await?;
186
187    let (func_qname, func_start_line) = match func_row {
188        Some(r) => r,
189        None => {
190            return Err(format!(
191                "no indexed function found containing lines {}-{} in {}; run `normalize structure rebuild`",
192                start_line, end_line, rel_path
193            ));
194        }
195    };
196
197    // Load all blocks for this function.
198    let block_rows = load_blocks(conn, &rel_path, &func_qname, func_start_line).await?;
199    let edge_rows = load_edges(conn, &rel_path, &func_qname, func_start_line).await?;
200    let def_rows = load_defs(conn, &rel_path, &func_qname, func_start_line).await?;
201    let use_rows = load_uses(conn, &rel_path, &func_qname, func_start_line).await?;
202    let effect_rows = load_effects(conn, &rel_path, &func_qname, func_start_line).await?;
203
204    // ── 3. Identify region blocks ─────────────────────────────────────────────
205    let region_block_ids: HashSet<u32> = block_rows
206        .iter()
207        .filter(|b| b.start_line <= end_line && b.end_line >= start_line)
208        .map(|b| b.block_id)
209        .collect();
210
211    if region_block_ids.is_empty() {
212        return Err(format!(
213            "no CFG blocks found overlapping lines {}-{}; the region may be outside any statement",
214            start_line, end_line
215        ));
216    }
217
218    // ── 4. Liveness fixed-point (whole function) ──────────────────────────────
219    let block_ids: Vec<u32> = block_rows.iter().map(|b| b.block_id).collect();
220
221    let mut defs: HashMap<u32, BTreeSet<String>> = HashMap::new();
222    let mut uses_map: HashMap<u32, BTreeSet<String>> = HashMap::new();
223    for id in &block_ids {
224        defs.insert(*id, BTreeSet::new());
225        uses_map.insert(*id, BTreeSet::new());
226    }
227    for (bid, name) in &def_rows {
228        defs.entry(*bid).or_default().insert(name.clone());
229    }
230    for (bid, name) in &use_rows {
231        uses_map.entry(*bid).or_default().insert(name.clone());
232    }
233
234    let mut succs: HashMap<u32, Vec<u32>> = HashMap::new();
235    for id in &block_ids {
236        succs.insert(*id, Vec::new());
237    }
238    for e in &edge_rows {
239        succs.entry(e.from).or_default().push(e.to);
240    }
241
242    let (live_in, live_out) = compute_liveness(&block_ids, &defs, &uses_map, &succs);
243
244    // ── 5. Derive parameters and return vars ──────────────────────────────────
245    let vars_defined_in_region: BTreeSet<String> = def_rows
246        .iter()
247        .filter(|(bid, _)| region_block_ids.contains(bid))
248        .map(|(_, name)| name.clone())
249        .collect();
250
251    let vars_defined_outside_region: BTreeSet<String> = def_rows
252        .iter()
253        .filter(|(bid, _)| !region_block_ids.contains(bid))
254        .map(|(_, name)| name.clone())
255        .collect();
256
257    // Params = live-in of region entry blocks that are defined outside the region.
258    // Region entry blocks = region blocks with no predecessors inside the region.
259    let region_entry_blocks: Vec<u32> = region_block_ids
260        .iter()
261        .cloned()
262        .filter(|bid| {
263            !edge_rows
264                .iter()
265                .any(|e| e.to == *bid && region_block_ids.contains(&e.from))
266        })
267        .collect();
268
269    let mut param_names: BTreeSet<String> = BTreeSet::new();
270    for bid in &region_entry_blocks {
271        if let Some(li) = live_in.get(bid) {
272            for v in li {
273                if vars_defined_outside_region.contains(v) {
274                    param_names.insert(v.clone());
275                }
276            }
277        }
278    }
279
280    // Return vars = live-out of region exit blocks that are defined inside the region.
281    // Region exit blocks = region blocks whose successors include a block outside the region.
282    let region_exit_blocks: Vec<u32> = region_block_ids
283        .iter()
284        .cloned()
285        .filter(|bid| {
286            succs
287                .get(bid)
288                .map(|ss| ss.iter().any(|s| !region_block_ids.contains(s)))
289                .unwrap_or(false)
290        })
291        .collect();
292
293    let mut return_var_names: BTreeSet<String> = BTreeSet::new();
294    for bid in &region_exit_blocks {
295        if let Some(lo) = live_out.get(bid) {
296            for v in lo {
297                if vars_defined_in_region.contains(v) {
298                    return_var_names.insert(v.clone());
299                }
300            }
301        }
302    }
303
304    // ── 6. Build Parameter structs ────────────────────────────────────────────
305    // We don't have type information in the index, but we can check if the
306    // variable's def site in the source text contains `mut` for Rust.
307    let parameters: Vec<Parameter> = param_names
308        .iter()
309        .map(|name| {
310            let mutable = is_mut_binding(&grammar_name, content, name);
311            let inferred_type = infer_type_from_annotation(&grammar_name, content, name);
312            Parameter {
313                name: name.clone(),
314                inferred_type,
315                mutable,
316            }
317        })
318        .collect();
319
320    // ── 7. Build return type ──────────────────────────────────────────────────
321    // Check for escaping exception edges.
322    let escaping_exceptions: Vec<(String, u32)> = edge_rows
323        .iter()
324        .filter(|e| {
325            e.kind == "exception"
326                && region_block_ids.contains(&e.from)
327                && !region_block_ids.contains(&e.to)
328        })
329        .filter_map(|e| e.exception_type.as_ref().map(|t| (t.clone(), e.from)))
330        .collect();
331
332    let return_type = if !escaping_exceptions.is_empty() && grammar_name == "rust" {
333        let ret_vars: Vec<String> = return_var_names.iter().cloned().collect();
334        let ok_type = if ret_vars.is_empty() {
335            "()".to_string()
336        } else if ret_vars.len() == 1 {
337            ret_vars[0].clone()
338        } else {
339            format!("({})", ret_vars.join(", "))
340        };
341        ReturnType::Result(ok_type, "Box<dyn std::error::Error>".to_string())
342    } else {
343        match return_var_names.len() {
344            0 => ReturnType::Unit,
345            1 => ReturnType::Single(return_var_names.iter().next().unwrap().clone()),
346            _ => {
347                let names: Vec<String> = return_var_names.iter().cloned().collect();
348                ReturnType::Tuple(names)
349            }
350        }
351    };
352
353    // ── 8. Effects analysis ───────────────────────────────────────────────────
354    let region_effects: Vec<&EffectRow> = effect_rows
355        .iter()
356        .filter(|e| region_block_ids.contains(&e.block_id))
357        .collect();
358
359    let is_async = region_effects.iter().any(|e| e.kind == "await");
360    let is_generator = region_effects.iter().any(|e| e.kind == "yield");
361
362    // ── 9. Build warnings ─────────────────────────────────────────────────────
363    let mut warnings: Vec<ExtractionWarning> = Vec::new();
364
365    for eff in &region_effects {
366        if eff.kind == "defer" {
367            warnings.push(ExtractionWarning::DeferCrossedBoundary { line: eff.line });
368        }
369    }
370
371    // Acquire without release in region.
372    for eff in &region_effects {
373        if eff.kind == "acquire" {
374            let label = eff
375                .label
376                .clone()
377                .unwrap_or_else(|| "<resource>".to_string());
378            let has_release = region_effects
379                .iter()
380                .any(|e| e.kind == "release" && e.label == eff.label);
381            if !has_release {
382                warnings.push(ExtractionWarning::ResourceLifetimeCrossedBoundary {
383                    label,
384                    acquire_line: eff.line,
385                });
386            }
387        }
388    }
389
390    for (exc_type, from_bid) in &escaping_exceptions {
391        // Find the throw line from cfg_effects or fall back to block start_line.
392        let throw_line = block_rows
393            .iter()
394            .find(|b| b.block_id == *from_bid)
395            .map(|b| b.start_line)
396            .unwrap_or(start_line);
397        warnings.push(ExtractionWarning::ExceptionEscapesBoundary {
398            exception_type: exc_type.clone(),
399            throw_line,
400        });
401    }
402
403    if let ReturnType::Tuple(ref names) = return_type {
404        // Some languages don't support multi-return natively.
405        if grammar_name != "go"
406            && grammar_name != "python"
407            && grammar_name != "typescript"
408            && grammar_name != "javascript"
409        {
410            warnings.push(ExtractionWarning::MultipleLiveOutVariables {
411                names: names.clone(),
412            });
413        }
414    }
415
416    // ── 10. Determine the extracted region's source text ──────────────────────
417    let lines: Vec<&str> = content.lines().collect();
418    let region_start_idx = (start_line.saturating_sub(1)) as usize;
419    let region_end_idx = (end_line as usize).min(lines.len());
420
421    if region_start_idx >= lines.len() {
422        return Err(format!(
423            "start line {} is beyond end of file ({} lines)",
424            start_line,
425            lines.len()
426        ));
427    }
428
429    let region_lines: Vec<&str> = lines[region_start_idx..region_end_idx].to_vec();
430
431    // Detect indentation of the call site (the first non-empty line of the region).
432    let call_site_indent = region_lines
433        .iter()
434        .find(|l| !l.trim().is_empty())
435        .map(|l| {
436            let trimmed = l.trim_start();
437            &l[..l.len() - trimmed.len()]
438        })
439        .unwrap_or("");
440
441    // Normalize the body: strip the common leading whitespace.
442    let body_lines = strip_common_indent(&region_lines);
443    let body_indent = "    "; // one level of indentation inside the new function
444
445    // ── 11. Generate code ─────────────────────────────────────────────────────
446    let new_function = generate_function(
447        &grammar_name,
448        function_name,
449        &parameters,
450        &return_type,
451        is_async,
452        is_generator,
453        &body_lines,
454        body_indent,
455    );
456
457    let call_site = generate_call_site(
458        &grammar_name,
459        function_name,
460        &parameters,
461        &return_type,
462        is_async,
463        call_site_indent,
464    );
465
466    // ── 12. Build the PlannedEdit ─────────────────────────────────────────────
467    // Replace the extracted lines with the call site.
468    // Insert the new function after the enclosing function.
469    let new_content = splice_content(content, start_line, end_line, &call_site, &new_function)?;
470
471    let plan = RefactoringPlan {
472        operation: "extract_function".to_string(),
473        edits: vec![PlannedEdit {
474            file: file_abs.to_path_buf(),
475            original: content.to_string(),
476            new_content,
477            description: format!("extract function '{}'", function_name),
478        }],
479        warnings: warnings.iter().map(|w| w.to_string_lossy()).collect(),
480    };
481
482    Ok(ExtractFunctionOutcome {
483        plan,
484        function_name: function_name.to_string(),
485        parameters,
486        return_type,
487        is_async,
488        is_generator,
489        call_site_line: start_line,
490    })
491}
492
493// ─── Index query helpers ──────────────────────────────────────────────────────
494
495async fn find_enclosing_function(
496    conn: &libsql::Connection,
497    file: &str,
498    start_line: u32,
499    end_line: u32,
500) -> Result<Option<(String, u32)>, String> {
501    // Find the function whose block range contains the selection.
502    // We pick the innermost function (maximum function_start_line) that spans the region.
503    let mut rows = conn
504        .query(
505            "SELECT function_qname, function_start_line \
506             FROM cfg_blocks \
507             WHERE file = ?1 \
508               AND start_line <= ?2 \
509               AND end_line >= ?3 \
510             ORDER BY function_start_line DESC \
511             LIMIT 1",
512            libsql::params![file.to_string(), start_line as i64, end_line as i64],
513        )
514        .await
515        .map_err(|e| format!("DB error: {}", e))?;
516
517    match rows.next().await.map_err(|e| format!("DB error: {}", e))? {
518        Some(row) => {
519            let qname: String = row.get(0).map_err(|e| format!("DB error: {}", e))?;
520            let fsl: i64 = row.get(1).map_err(|e| format!("DB error: {}", e))?;
521            Ok(Some((qname, fsl as u32)))
522        }
523        None => Ok(None),
524    }
525}
526
527async fn load_blocks(
528    conn: &libsql::Connection,
529    file: &str,
530    func_qname: &str,
531    func_start_line: u32,
532) -> Result<Vec<BlockRow>, String> {
533    let mut rows = conn
534        .query(
535            "SELECT block_id, kind, start_line, end_line \
536             FROM cfg_blocks \
537             WHERE file = ?1 AND function_qname = ?2 AND function_start_line = ?3 \
538             ORDER BY block_id",
539            libsql::params![
540                file.to_string(),
541                func_qname.to_string(),
542                func_start_line as i64
543            ],
544        )
545        .await
546        .map_err(|e| format!("DB error: {}", e))?;
547
548    let mut out = Vec::new();
549    while let Some(row) = rows.next().await.map_err(|e| format!("DB error: {}", e))? {
550        let block_id: i64 = row.get(0).map_err(|e| format!("DB error: {}", e))?;
551        let kind: String = row.get(1).map_err(|e| format!("DB error: {}", e))?;
552        let start_line: i64 = row.get(2).map_err(|e| format!("DB error: {}", e))?;
553        let end_line: i64 = row.get(3).map_err(|e| format!("DB error: {}", e))?;
554        out.push(BlockRow {
555            block_id: block_id as u32,
556            kind,
557            start_line: start_line as u32,
558            end_line: end_line as u32,
559        });
560    }
561    Ok(out)
562}
563
564async fn load_edges(
565    conn: &libsql::Connection,
566    file: &str,
567    func_qname: &str,
568    func_start_line: u32,
569) -> Result<Vec<EdgeRow>, String> {
570    let mut rows = conn
571        .query(
572            "SELECT from_block, to_block, kind, COALESCE(exception_type, '') \
573             FROM cfg_edges \
574             WHERE file = ?1 AND function_qname = ?2 AND function_start_line = ?3",
575            libsql::params![
576                file.to_string(),
577                func_qname.to_string(),
578                func_start_line as i64
579            ],
580        )
581        .await
582        .map_err(|e| format!("DB error: {}", e))?;
583
584    let mut out = Vec::new();
585    while let Some(row) = rows.next().await.map_err(|e| format!("DB error: {}", e))? {
586        let from: i64 = row.get(0).map_err(|e| format!("DB error: {}", e))?;
587        let to: i64 = row.get(1).map_err(|e| format!("DB error: {}", e))?;
588        let kind: String = row.get(2).map_err(|e| format!("DB error: {}", e))?;
589        let exc: String = row.get(3).map_err(|e| format!("DB error: {}", e))?;
590        out.push(EdgeRow {
591            from: from as u32,
592            to: to as u32,
593            kind,
594            exception_type: if exc.is_empty() { None } else { Some(exc) },
595        });
596    }
597    Ok(out)
598}
599
600async fn load_defs(
601    conn: &libsql::Connection,
602    file: &str,
603    func_qname: &str,
604    func_start_line: u32,
605) -> Result<Vec<(u32, String)>, String> {
606    let mut rows = conn
607        .query(
608            "SELECT block_id, name \
609             FROM cfg_defs \
610             WHERE file = ?1 AND function_qname = ?2 AND function_start_line = ?3",
611            libsql::params![
612                file.to_string(),
613                func_qname.to_string(),
614                func_start_line as i64
615            ],
616        )
617        .await
618        .map_err(|e| format!("DB error: {}", e))?;
619
620    let mut out = Vec::new();
621    while let Some(row) = rows.next().await.map_err(|e| format!("DB error: {}", e))? {
622        let block_id: i64 = row.get(0).map_err(|e| format!("DB error: {}", e))?;
623        let name: String = row.get(1).map_err(|e| format!("DB error: {}", e))?;
624        out.push((block_id as u32, name));
625    }
626    Ok(out)
627}
628
629async fn load_uses(
630    conn: &libsql::Connection,
631    file: &str,
632    func_qname: &str,
633    func_start_line: u32,
634) -> Result<Vec<(u32, String)>, String> {
635    let mut rows = conn
636        .query(
637            "SELECT block_id, name \
638             FROM cfg_uses \
639             WHERE file = ?1 AND function_qname = ?2 AND function_start_line = ?3",
640            libsql::params![
641                file.to_string(),
642                func_qname.to_string(),
643                func_start_line as i64
644            ],
645        )
646        .await
647        .map_err(|e| format!("DB error: {}", e))?;
648
649    let mut out = Vec::new();
650    while let Some(row) = rows.next().await.map_err(|e| format!("DB error: {}", e))? {
651        let block_id: i64 = row.get(0).map_err(|e| format!("DB error: {}", e))?;
652        let name: String = row.get(1).map_err(|e| format!("DB error: {}", e))?;
653        out.push((block_id as u32, name));
654    }
655    Ok(out)
656}
657
658async fn load_effects(
659    conn: &libsql::Connection,
660    file: &str,
661    func_qname: &str,
662    func_start_line: u32,
663) -> Result<Vec<EffectRow>, String> {
664    let mut rows = conn
665        .query(
666            "SELECT block_id, kind, line, COALESCE(label, '') \
667             FROM cfg_effects \
668             WHERE file = ?1 AND function_qname = ?2 AND function_start_line = ?3",
669            libsql::params![
670                file.to_string(),
671                func_qname.to_string(),
672                func_start_line as i64
673            ],
674        )
675        .await
676        .map_err(|e| format!("DB error: {}", e))?;
677
678    let mut out = Vec::new();
679    while let Some(row) = rows.next().await.map_err(|e| format!("DB error: {}", e))? {
680        let block_id: i64 = row.get(0).map_err(|e| format!("DB error: {}", e))?;
681        let kind: String = row.get(1).map_err(|e| format!("DB error: {}", e))?;
682        let line: i64 = row.get(2).map_err(|e| format!("DB error: {}", e))?;
683        let label: String = row.get(3).map_err(|e| format!("DB error: {}", e))?;
684        out.push(EffectRow {
685            block_id: block_id as u32,
686            kind,
687            line: line as u32,
688            label: if label.is_empty() { None } else { Some(label) },
689        });
690    }
691    Ok(out)
692}
693
694// ─── Liveness ─────────────────────────────────────────────────────────────────
695
696fn compute_liveness(
697    block_ids: &[u32],
698    defs: &HashMap<u32, BTreeSet<String>>,
699    uses_map: &HashMap<u32, BTreeSet<String>>,
700    succs: &HashMap<u32, Vec<u32>>,
701    // normalize-syntax-allow: rust/tuple-return
702) -> (
703    HashMap<u32, BTreeSet<String>>,
704    HashMap<u32, BTreeSet<String>>,
705) {
706    let mut live_in: HashMap<u32, BTreeSet<String>> = HashMap::new();
707    let mut live_out: HashMap<u32, BTreeSet<String>> = HashMap::new();
708    for id in block_ids {
709        live_in.insert(*id, BTreeSet::new());
710        live_out.insert(*id, BTreeSet::new());
711    }
712
713    let empty: BTreeSet<String> = BTreeSet::new();
714    let mut changed = true;
715    while changed {
716        changed = false;
717        for &bid in block_ids.iter().rev() {
718            let mut new_lo: BTreeSet<String> = BTreeSet::new();
719            if let Some(succ_list) = succs.get(&bid) {
720                for &s in succ_list {
721                    if let Some(li) = live_in.get(&s) {
722                        new_lo.extend(li.iter().cloned());
723                    }
724                }
725            }
726
727            let block_uses = uses_map.get(&bid).unwrap_or(&empty);
728            let block_defs = defs.get(&bid).unwrap_or(&empty);
729            let mut new_li: BTreeSet<String> = block_uses.clone();
730            for v in &new_lo {
731                if !block_defs.contains(v) {
732                    new_li.insert(v.clone());
733                }
734            }
735
736            if new_lo != *live_out.get(&bid).unwrap_or(&empty)
737                || new_li != *live_in.get(&bid).unwrap_or(&empty)
738            {
739                changed = true;
740                live_out.insert(bid, new_lo);
741                live_in.insert(bid, new_li);
742            }
743        }
744    }
745
746    (live_in, live_out)
747}
748
749// ─── Source analysis helpers ──────────────────────────────────────────────────
750
751/// Check whether a variable binding in the source uses `mut` (Rust-specific).
752fn is_mut_binding(grammar: &str, content: &str, name: &str) -> bool {
753    if grammar != "rust" {
754        return false;
755    }
756    // Heuristic: look for `let mut <name>` in the source.
757    // This is a text scan; the index doesn't store mutability.
758    let pattern = format!("let mut {}", name);
759    content.contains(&pattern)
760}
761
762/// Try to extract a type annotation for a variable from the source (best-effort).
763/// Returns `None` when no annotation is found.
764fn infer_type_from_annotation(grammar: &str, content: &str, name: &str) -> Option<String> {
765    match grammar {
766        "rust" => {
767            // Look for `let [mut] <name>: <type>` or `<name>: <type>` in function params.
768            // Simple heuristic: find `<name>: ` and grab the type until `,` or `)` or `=`.
769            let pattern = format!("{}: ", name);
770            if let Some(pos) = content.find(&pattern) {
771                let after = &content[pos + pattern.len()..];
772                let end = after.find([',', ')', '=', '\n']).unwrap_or(after.len());
773                let ty = after[..end].trim().to_string();
774                if !ty.is_empty() && !ty.contains(' ') {
775                    return Some(ty);
776                }
777            }
778            None
779        }
780        "typescript" => {
781            // Look for `<name>: <type>` in parameter lists.
782            let pattern = format!("{}: ", name);
783            if let Some(pos) = content.find(&pattern) {
784                let after = &content[pos + pattern.len()..];
785                let end = after.find([',', ')', '=', '\n']).unwrap_or(after.len());
786                let ty = after[..end].trim().to_string();
787                if !ty.is_empty() {
788                    return Some(ty);
789                }
790            }
791            None
792        }
793        _ => None,
794    }
795}
796
797// ─── Code generation ──────────────────────────────────────────────────────────
798
799#[allow(clippy::too_many_arguments)]
800fn generate_function(
801    grammar: &str,
802    name: &str,
803    params: &[Parameter],
804    ret: &ReturnType,
805    is_async: bool,
806    is_generator: bool,
807    body_lines: &[String],
808    indent: &str,
809) -> String {
810    match grammar {
811        "python" => generate_python_function(name, params, ret, is_async, body_lines, indent),
812        "go" => generate_go_function(name, params, ret, body_lines, indent),
813        "typescript" | "javascript" | "tsx" | "jsx" => {
814            generate_ts_function(grammar, name, params, ret, is_async, body_lines, indent)
815        }
816        "java" => generate_java_function(name, params, ret, body_lines, indent),
817        _ => {
818            // Default: Rust
819            generate_rust_function(
820                name,
821                params,
822                ret,
823                is_async,
824                is_generator,
825                body_lines,
826                indent,
827            )
828        }
829    }
830}
831
832fn generate_rust_function(
833    name: &str,
834    params: &[Parameter],
835    ret: &ReturnType,
836    is_async: bool,
837    _is_generator: bool,
838    body_lines: &[String],
839    indent: &str,
840) -> String {
841    let async_kw = if is_async { "async " } else { "" };
842    let param_str = params
843        .iter()
844        .map(|p| {
845            let mut_kw = if p.mutable { "mut " } else { "" };
846            match &p.inferred_type {
847                Some(ty) => format!("{}{}: {}", mut_kw, p.name, ty),
848                None => format!("{}{}: /* type */", mut_kw, p.name),
849            }
850        })
851        .collect::<Vec<_>>()
852        .join(", ");
853    let ret_str = match ret {
854        ReturnType::Unit => String::new(),
855        ReturnType::Single(v) => format!(" -> /* {} */", v),
856        ReturnType::Tuple(vs) => format!(" -> /* ({}) */", vs.join(", ")),
857        ReturnType::Result(ok, err) => format!(" -> Result</* {} */, {}>", ok, err),
858    };
859    let return_stmt = match ret {
860        ReturnType::Unit => String::new(),
861        ReturnType::Single(v) => format!("\n{}    {}", indent, v),
862        ReturnType::Tuple(vs) => format!("\n{}    ({})", indent, vs.join(", ")),
863        ReturnType::Result(ok, _) => {
864            if ok == "()" {
865                format!("\n{}    Ok(())", indent)
866            } else {
867                format!("\n{}    Ok({})", indent, ok)
868            }
869        }
870    };
871
872    let body = body_lines
873        .iter()
874        .map(|l| format!("{}    {}", indent, l))
875        .collect::<Vec<_>>()
876        .join("\n");
877
878    format!(
879        "\n{}{}fn {}({}){} {{\n{}{}\n{}}}\n",
880        indent, async_kw, name, param_str, ret_str, body, return_stmt, indent
881    )
882}
883
884fn generate_python_function(
885    name: &str,
886    params: &[Parameter],
887    ret: &ReturnType,
888    is_async: bool,
889    body_lines: &[String],
890    indent: &str,
891) -> String {
892    let async_kw = if is_async { "async " } else { "" };
893    let param_str = params
894        .iter()
895        .map(|p| p.name.clone())
896        .collect::<Vec<_>>()
897        .join(", ");
898    let return_stmt = match ret {
899        ReturnType::Unit => String::new(),
900        ReturnType::Single(v) => format!("\n{}    return {}", indent, v),
901        ReturnType::Tuple(vs) => format!("\n{}    return {}", indent, vs.join(", ")),
902        ReturnType::Result(ok, _) => format!("\n{}    return {}", indent, ok),
903    };
904
905    let body = body_lines
906        .iter()
907        .map(|l| format!("{}    {}", indent, l))
908        .collect::<Vec<_>>()
909        .join("\n");
910
911    format!(
912        "\n{}{}def {}({}):\n{}{}\n",
913        indent, async_kw, name, param_str, body, return_stmt
914    )
915}
916
917fn generate_go_function(
918    name: &str,
919    params: &[Parameter],
920    ret: &ReturnType,
921    body_lines: &[String],
922    indent: &str,
923) -> String {
924    // Go: capitalise first letter for exported, lowercase for unexported.
925    let param_str = params
926        .iter()
927        .map(|p| match &p.inferred_type {
928            Some(ty) => format!("{} {}", p.name, ty),
929            None => format!("{} interface{{}}", p.name),
930        })
931        .collect::<Vec<_>>()
932        .join(", ");
933    let ret_str = match ret {
934        ReturnType::Unit => String::new(),
935        ReturnType::Single(v) => format!(" /* {} */", v),
936        ReturnType::Tuple(vs) => format!(" ({} /* multi-return */)", vs.join(", ")),
937        ReturnType::Result(ok, _) => format!(" ({}, error)", ok),
938    };
939    let return_stmt = match ret {
940        ReturnType::Unit => String::new(),
941        ReturnType::Single(v) => format!("\n{}    return {}", indent, v),
942        ReturnType::Tuple(vs) => format!("\n{}    return {}", indent, vs.join(", ")),
943        ReturnType::Result(ok, _) => format!("\n{}    return {}, nil", indent, ok),
944    };
945
946    let body = body_lines
947        .iter()
948        .map(|l| format!("{}    {}", indent, l))
949        .collect::<Vec<_>>()
950        .join("\n");
951
952    format!(
953        "\n{}func {}({}){} {{\n{}{}\n{}}}\n",
954        indent, name, param_str, ret_str, body, return_stmt, indent
955    )
956}
957
958fn generate_ts_function(
959    grammar: &str,
960    name: &str,
961    params: &[Parameter],
962    ret: &ReturnType,
963    is_async: bool,
964    body_lines: &[String],
965    indent: &str,
966) -> String {
967    let async_kw = if is_async { "async " } else { "" };
968    let param_str = params
969        .iter()
970        .map(|p| match &p.inferred_type {
971            Some(ty) if grammar == "typescript" || grammar == "tsx" => {
972                format!("{}: {}", p.name, ty)
973            }
974            _ => p.name.clone(),
975        })
976        .collect::<Vec<_>>()
977        .join(", ");
978
979    let ret_annotation = if grammar == "typescript" || grammar == "tsx" {
980        match ret {
981            ReturnType::Unit => ": void".to_string(),
982            ReturnType::Single(v) => format!(": /* {} */", v),
983            ReturnType::Tuple(vs) => format!(
984                ": [{}]",
985                vs.iter()
986                    .map(|v| format!("/* {} */", v))
987                    .collect::<Vec<_>>()
988                    .join(", ")
989            ),
990            ReturnType::Result(ok, _) => format!(": {} | Error", ok),
991        }
992    } else {
993        String::new()
994    };
995
996    let return_stmt = match ret {
997        ReturnType::Unit => String::new(),
998        ReturnType::Single(v) => format!("\n{}    return {};", indent, v),
999        ReturnType::Tuple(vs) => format!("\n{}    return [{}];", indent, vs.join(", ")),
1000        ReturnType::Result(ok, _) => format!("\n{}    return {};", indent, ok),
1001    };
1002
1003    let body = body_lines
1004        .iter()
1005        .map(|l| format!("{}    {}", indent, l))
1006        .collect::<Vec<_>>()
1007        .join("\n");
1008
1009    format!(
1010        "\n{}{}function {}({}){} {{\n{}{}\n{}}}\n",
1011        indent, async_kw, name, param_str, ret_annotation, body, return_stmt, indent
1012    )
1013}
1014
1015fn generate_java_function(
1016    name: &str,
1017    params: &[Parameter],
1018    ret: &ReturnType,
1019    body_lines: &[String],
1020    indent: &str,
1021) -> String {
1022    let ret_type = match ret {
1023        ReturnType::Unit => "void".to_string(),
1024        ReturnType::Single(v) => format!("/* {} */", v),
1025        ReturnType::Tuple(vs) => format!("/* TODO: struct({}) */", vs.join(", ")),
1026        ReturnType::Result(ok, err) => format!("/* {} throws {} */", ok, err),
1027    };
1028    let param_str = params
1029        .iter()
1030        .map(|p| match &p.inferred_type {
1031            Some(ty) => format!("{} {}", ty, p.name),
1032            None => format!("/* type */ {}", p.name),
1033        })
1034        .collect::<Vec<_>>()
1035        .join(", ");
1036    let return_stmt = match ret {
1037        ReturnType::Unit => String::new(),
1038        ReturnType::Single(v) => format!("\n{}    return {};", indent, v),
1039        ReturnType::Tuple(vs) => {
1040            format!("\n{}    // TODO: return struct({});", indent, vs.join(", "))
1041        }
1042        ReturnType::Result(ok, _) => format!("\n{}    return {};", indent, ok),
1043    };
1044
1045    let body = body_lines
1046        .iter()
1047        .map(|l| format!("{}    {}", indent, l))
1048        .collect::<Vec<_>>()
1049        .join("\n");
1050
1051    format!(
1052        "\n{}private {} {}({}) {{\n{}{}\n{}}}\n",
1053        indent, ret_type, name, param_str, body, return_stmt, indent
1054    )
1055}
1056
1057fn generate_call_site(
1058    grammar: &str,
1059    name: &str,
1060    params: &[Parameter],
1061    ret: &ReturnType,
1062    is_async: bool,
1063    indent: &str,
1064) -> String {
1065    let args = params
1066        .iter()
1067        .map(|p| p.name.as_str())
1068        .collect::<Vec<_>>()
1069        .join(", ");
1070    let await_kw = if is_async {
1071        match grammar {
1072            "rust" => ".await",
1073            _ => "await ",
1074        }
1075    } else {
1076        ""
1077    };
1078
1079    match grammar {
1080        "python" => match ret {
1081            ReturnType::Unit => format!(
1082                "{}{}{}{}",
1083                indent,
1084                await_kw,
1085                name,
1086                format_args!("({})", args)
1087            ),
1088            ReturnType::Single(v) => {
1089                format!("{}{} = {}{}({})\n", indent, v, await_kw, name, args)
1090            }
1091            ReturnType::Tuple(vs) => {
1092                format!(
1093                    "{}{} = {}{}({})\n",
1094                    indent,
1095                    vs.join(", "),
1096                    await_kw,
1097                    name,
1098                    args
1099                )
1100            }
1101            ReturnType::Result(ok, _) => format!("{}{} = {}({})\n", indent, ok, name, args),
1102        },
1103        "go" => match ret {
1104            ReturnType::Unit => format!("{}{}({})\n", indent, name, args),
1105            ReturnType::Single(v) => format!("{}{} := {}({})\n", indent, v, name, args),
1106            ReturnType::Tuple(vs) => {
1107                format!("{}{} := {}({})\n", indent, vs.join(", "), name, args)
1108            }
1109            ReturnType::Result(ok, _) => {
1110                format!(
1111                    "{}{}, err := {}({})\n{}if err != nil {{ return err }}\n",
1112                    indent, ok, name, args, indent
1113                )
1114            }
1115        },
1116        "typescript" | "javascript" | "tsx" | "jsx" => match ret {
1117            ReturnType::Unit => format!(
1118                "{}{}{}{}",
1119                indent,
1120                await_kw,
1121                name,
1122                format_args!("({});", args)
1123            ),
1124            ReturnType::Single(v) => {
1125                let prefix = if await_kw == "await " { "await " } else { "" };
1126                format!("{}const {} = {}{}({});\n", indent, v, prefix, name, args)
1127            }
1128            ReturnType::Tuple(vs) => {
1129                let prefix = if await_kw == "await " { "await " } else { "" };
1130                format!(
1131                    "{}const [{}] = {}{}({});\n",
1132                    indent,
1133                    vs.join(", "),
1134                    prefix,
1135                    name,
1136                    args
1137                )
1138            }
1139            ReturnType::Result(ok, _) => {
1140                let prefix = if await_kw == "await " { "await " } else { "" };
1141                format!("{}const {} = {}{}({});\n", indent, ok, prefix, name, args)
1142            }
1143        },
1144        "java" => match ret {
1145            ReturnType::Unit => format!("{}{}({});\n", indent, name, args),
1146            ReturnType::Single(v) => format!("{}var {} = {}({});\n", indent, v, name, args),
1147            ReturnType::Tuple(vs) => {
1148                format!(
1149                    "{}var result = {}({}); // TODO: unpack ({})\n",
1150                    indent,
1151                    name,
1152                    args,
1153                    vs.join(", ")
1154                )
1155            }
1156            ReturnType::Result(ok, _) => format!("{}var {} = {}({});\n", indent, ok, name, args),
1157        },
1158        _ => {
1159            // Rust
1160            match ret {
1161                ReturnType::Unit => {
1162                    if is_async {
1163                        format!("{}{}({}).await;\n", indent, name, args)
1164                    } else {
1165                        format!("{}{}({});\n", indent, name, args)
1166                    }
1167                }
1168                ReturnType::Single(v) => {
1169                    if is_async {
1170                        format!("{}let {} = {}({}).await;\n", indent, v, name, args)
1171                    } else {
1172                        format!("{}let {} = {}({});\n", indent, v, name, args)
1173                    }
1174                }
1175                ReturnType::Tuple(vs) => {
1176                    if is_async {
1177                        format!(
1178                            "{}let ({}) = {}({}).await;\n",
1179                            indent,
1180                            vs.join(", "),
1181                            name,
1182                            args
1183                        )
1184                    } else {
1185                        format!("{}let ({}) = {}({});\n", indent, vs.join(", "), name, args)
1186                    }
1187                }
1188                ReturnType::Result(ok, _) => {
1189                    if is_async {
1190                        format!("{}let {} = {}({}).await?;\n", indent, ok, name, args)
1191                    } else {
1192                        format!("{}let {} = {}({})?;\n", indent, ok, name, args)
1193                    }
1194                }
1195            }
1196        }
1197    }
1198}
1199
1200// ─── Content splicing ─────────────────────────────────────────────────────────
1201
1202/// Strip the common leading whitespace from a set of lines.
1203fn strip_common_indent(lines: &[&str]) -> Vec<String> {
1204    let min_indent = lines
1205        .iter()
1206        .filter(|l| !l.trim().is_empty())
1207        .map(|l| l.len() - l.trim_start().len())
1208        .min()
1209        .unwrap_or(0);
1210
1211    lines
1212        .iter()
1213        .map(|l| {
1214            if l.len() >= min_indent {
1215                l[min_indent..].to_string()
1216            } else {
1217                l.to_string()
1218            }
1219        })
1220        .collect()
1221}
1222
1223/// Replace lines `start_line..=end_line` (1-based) in `content` with `call_site_text`,
1224/// and append `new_function` after the enclosing function's closing brace.
1225///
1226/// Simple strategy:
1227/// - Replace the region lines with the call site.
1228/// - Append the new function just after the end of the file (or after the
1229///   enclosing function). We use end-of-file for simplicity; a future version
1230///   can find the enclosing function's closing brace.
1231fn splice_content(
1232    content: &str,
1233    start_line: u32,
1234    end_line: u32,
1235    call_site: &str,
1236    new_function: &str,
1237) -> Result<String, String> {
1238    let lines: Vec<&str> = content.lines().collect();
1239    let n = lines.len();
1240
1241    let start_idx = (start_line.saturating_sub(1)) as usize;
1242    let end_idx = (end_line as usize).min(n);
1243
1244    if start_idx >= n {
1245        return Err(format!(
1246            "start line {} is beyond end of file ({} lines)",
1247            start_line, n
1248        ));
1249    }
1250
1251    let mut new_lines: Vec<&str> = Vec::new();
1252    new_lines.extend_from_slice(&lines[..start_idx]);
1253
1254    // Insert call site lines (drop trailing newline from the generated text).
1255    let call_site_trimmed = call_site.trim_end_matches('\n');
1256    for l in call_site_trimmed.lines() {
1257        new_lines.push(l);
1258    }
1259
1260    new_lines.extend_from_slice(&lines[end_idx..]);
1261
1262    // Preserve trailing newline behaviour.
1263    let had_trailing = content.ends_with('\n');
1264    let mut result = new_lines.join("\n");
1265    if had_trailing {
1266        result.push('\n');
1267    }
1268
1269    // Append the new function at the end of the file.
1270    result.push_str(new_function);
1271
1272    Ok(result)
1273}
1274
1275// ─── Public helper: parse "start-end" line range ──────────────────────────────
1276
1277/// Parse a `"start-end"` line-range string (e.g. `"10-25"`) into `(start, end)`.
1278/// Lines are 1-based inclusive.
1279pub fn parse_line_range(s: &str) -> Result<(u32, u32), String> {
1280    match s.split_once('-') {
1281        Some((a, b)) => {
1282            let start = a
1283                .trim()
1284                .parse::<u32>()
1285                .map_err(|_| format!("invalid start line in range '{}': expected integer", s))?;
1286            let end = b
1287                .trim()
1288                .parse::<u32>()
1289                .map_err(|_| format!("invalid end line in range '{}': expected integer", s))?;
1290            if start == 0 || end == 0 {
1291                return Err(format!(
1292                    "line range '{}': lines are 1-based (must be ≥ 1)",
1293                    s
1294                ));
1295            }
1296            if start > end {
1297                return Err(format!(
1298                    "line range '{}': start ({}) must be ≤ end ({})",
1299                    s, start, end
1300                ));
1301            }
1302            Ok((start, end))
1303        }
1304        None => Err(format!(
1305            "invalid line range '{}': expected 'start-end' (e.g. '10-25')",
1306            s
1307        )),
1308    }
1309}
1310
1311// ─── Tests ────────────────────────────────────────────────────────────────────
1312
1313#[cfg(test)]
1314mod tests {
1315    use super::*;
1316
1317    // ── parse_line_range ──────────────────────────────────────────────────────
1318
1319    #[test]
1320    fn parse_line_range_basic() {
1321        assert_eq!(parse_line_range("10-25").unwrap(), (10, 25));
1322    }
1323
1324    #[test]
1325    fn parse_line_range_single_line() {
1326        assert_eq!(parse_line_range("5-5").unwrap(), (5, 5));
1327    }
1328
1329    #[test]
1330    fn parse_line_range_rejects_zero() {
1331        assert!(parse_line_range("0-5").is_err());
1332    }
1333
1334    #[test]
1335    fn parse_line_range_rejects_inverted() {
1336        assert!(parse_line_range("10-5").is_err());
1337    }
1338
1339    #[test]
1340    fn parse_line_range_rejects_missing_dash() {
1341        assert!(parse_line_range("10").is_err());
1342    }
1343
1344    // ── strip_common_indent ───────────────────────────────────────────────────
1345
1346    #[test]
1347    fn strip_common_indent_basic() {
1348        let lines = vec!["    let x = 1;", "    let y = 2;"];
1349        let out = strip_common_indent(&lines);
1350        assert_eq!(out, vec!["let x = 1;", "let y = 2;"]);
1351    }
1352
1353    #[test]
1354    fn strip_common_indent_mixed() {
1355        let lines = vec!["    let x = 1;", "        let y = 2;"];
1356        let out = strip_common_indent(&lines);
1357        assert_eq!(out, vec!["let x = 1;", "    let y = 2;"]);
1358    }
1359
1360    // ── generate_rust_function ────────────────────────────────────────────────
1361
1362    #[test]
1363    fn generate_rust_fn_unit_return() {
1364        let params = vec![Parameter {
1365            name: "x".to_string(),
1366            inferred_type: Some("i32".to_string()),
1367            mutable: false,
1368        }];
1369        let body = vec!["println!(\"{}\", x);".to_string()];
1370        let out = generate_rust_function(
1371            "do_thing",
1372            &params,
1373            &ReturnType::Unit,
1374            false,
1375            false,
1376            &body,
1377            "",
1378        );
1379        assert!(out.contains("fn do_thing(x: i32)"));
1380        assert!(out.contains("println!"));
1381        assert!(!out.contains("->"));
1382    }
1383
1384    #[test]
1385    fn generate_rust_fn_single_return() {
1386        let params = vec![];
1387        let body = vec!["let result = 42;".to_string()];
1388        let out = generate_rust_function(
1389            "compute",
1390            &params,
1391            &ReturnType::Single("result".to_string()),
1392            false,
1393            false,
1394            &body,
1395            "",
1396        );
1397        assert!(out.contains("fn compute()"));
1398        assert!(out.contains("-> /* result */"));
1399        assert!(out.contains("result"));
1400    }
1401
1402    #[test]
1403    fn generate_rust_fn_async() {
1404        let params = vec![];
1405        let body = vec!["tokio::time::sleep(Duration::from_secs(1)).await;".to_string()];
1406        let out = generate_rust_function(
1407            "wait_a_bit",
1408            &params,
1409            &ReturnType::Unit,
1410            true,
1411            false,
1412            &body,
1413            "",
1414        );
1415        assert!(out.contains("async fn wait_a_bit()"));
1416    }
1417
1418    // ── generate_python_function ──────────────────────────────────────────────
1419
1420    #[test]
1421    fn generate_python_fn_basic() {
1422        let params = vec![Parameter {
1423            name: "x".to_string(),
1424            inferred_type: None,
1425            mutable: false,
1426        }];
1427        let body = vec!["print(x)".to_string()];
1428        let out = generate_python_function("show", &params, &ReturnType::Unit, false, &body, "");
1429        assert!(out.contains("def show(x):"));
1430        assert!(out.contains("print(x)"));
1431    }
1432
1433    #[test]
1434    fn generate_python_fn_multi_return() {
1435        let params = vec![];
1436        let body = vec!["a = 1".to_string(), "b = 2".to_string()];
1437        let out = generate_python_function(
1438            "two_values",
1439            &params,
1440            &ReturnType::Tuple(vec!["a".to_string(), "b".to_string()]),
1441            false,
1442            &body,
1443            "",
1444        );
1445        assert!(out.contains("return a, b"));
1446    }
1447
1448    // ── generate_go_function ──────────────────────────────────────────────────
1449
1450    #[test]
1451    fn generate_go_fn_basic() {
1452        let params = vec![Parameter {
1453            name: "n".to_string(),
1454            inferred_type: Some("int".to_string()),
1455            mutable: false,
1456        }];
1457        let body = vec!["result := n * 2".to_string()];
1458        let out = generate_go_function(
1459            "double",
1460            &params,
1461            &ReturnType::Single("result".to_string()),
1462            &body,
1463            "",
1464        );
1465        assert!(out.contains("func double(n int)"));
1466        assert!(out.contains("return result"));
1467    }
1468
1469    // ── splice_content ────────────────────────────────────────────────────────
1470
1471    #[test]
1472    fn splice_content_replaces_region() {
1473        let content = "line1\nline2\nline3\nline4\nline5\n";
1474        let result =
1475            splice_content(content, 2, 3, "    call()\n", "\nfn extracted() {}\n").unwrap();
1476        assert!(result.contains("line1\n    call()\nline4\nline5\n"));
1477        assert!(result.contains("fn extracted()"));
1478    }
1479
1480    #[test]
1481    fn splice_content_preserves_surrounding_lines() {
1482        let content = "a\nb\nc\nd\n";
1483        let result = splice_content(content, 2, 2, "X\n", "\nfn f() {}\n").unwrap();
1484        assert!(result.starts_with("a\nX\nc\nd\n"));
1485    }
1486
1487    // ── call site generation ──────────────────────────────────────────────────
1488
1489    #[test]
1490    fn rust_call_site_with_return() {
1491        let params = vec![Parameter {
1492            name: "x".to_string(),
1493            inferred_type: Some("i32".to_string()),
1494            mutable: false,
1495        }];
1496        let call = generate_call_site(
1497            "rust",
1498            "compute",
1499            &params,
1500            &ReturnType::Single("result".to_string()),
1501            false,
1502            "    ",
1503        );
1504        assert_eq!(call, "    let result = compute(x);\n");
1505    }
1506
1507    #[test]
1508    fn rust_call_site_async() {
1509        let params = vec![];
1510        let call = generate_call_site(
1511            "rust",
1512            "fetch",
1513            &params,
1514            &ReturnType::Single("data".to_string()),
1515            true,
1516            "    ",
1517        );
1518        assert_eq!(call, "    let data = fetch().await;\n");
1519    }
1520
1521    #[test]
1522    fn python_call_site_multi_return() {
1523        let params = vec![];
1524        let call = generate_call_site(
1525            "python",
1526            "two_vals",
1527            &params,
1528            &ReturnType::Tuple(vec!["a".to_string(), "b".to_string()]),
1529            false,
1530            "",
1531        );
1532        assert_eq!(call, "a, b = two_vals()\n");
1533    }
1534}