Skip to main content

logicaffeine_compile/codegen/
detection.rs

1use std::collections::{HashMap, HashSet};
2
3use crate::analysis::registry::{FieldType, TypeDef, TypeRegistry};
4use crate::ast::stmt::{Expr, Literal, ReadSource, Stmt, TypeExpr};
5use crate::intern::{Interner, Symbol};
6
7use super::is_recursive_field;
8use super::types::codegen_type_expr;
9
10pub(super) fn is_result_type(ty: &TypeExpr, interner: &Interner) -> bool {
11    if let TypeExpr::Generic { base, .. } = ty {
12        interner.resolve(*base) == "Result"
13    } else {
14        false
15    }
16}
17
18/// Phase 51: Detect if any statements require async execution.
19/// Returns true if the program needs #[tokio::main] async fn main().
20pub(super) fn requires_async(stmts: &[Stmt]) -> bool {
21    stmts.iter().any(|s| requires_async_stmt(s))
22}
23
24pub(super) fn requires_async_stmt(stmt: &Stmt) -> bool {
25    match stmt {
26        // Phase 9: Concurrent blocks use tokio::join!
27        Stmt::Concurrent { tasks } => true,
28        // Phase 51: Network operations and Sleep are async
29        Stmt::Listen { .. } => true,
30        Stmt::ConnectTo { .. } => true,
31        Stmt::Sleep { .. } => true,
32        // Phase 52: Sync is async (GossipSub subscription)
33        Stmt::Sync { .. } => true,
34        // Phase 53: Mount is async (VFS file operations)
35        Stmt::Mount { .. } => true,
36        // Phase 53: File I/O is async (VFS operations)
37        Stmt::ReadFrom { source: ReadSource::File(_), .. } => true,
38        Stmt::WriteFile { .. } => true,
39        // Phase 54: Go-like concurrency is async
40        Stmt::LaunchTask { .. } => true,
41        Stmt::LaunchTaskWithHandle { .. } => true,
42        Stmt::SendPipe { .. } => true,
43        Stmt::ReceivePipe { .. } => true,
44        Stmt::Select { .. } => true,
45        // While and Repeat are now always async due to check_preemption()
46        // (handled below in recursive check)
47        // Recursively check nested blocks
48        Stmt::If { then_block, else_block, .. } => {
49            then_block.iter().any(|s| requires_async_stmt(s))
50                || else_block.map_or(false, |b| b.iter().any(|s| requires_async_stmt(s)))
51        }
52        Stmt::While { body, .. } => body.iter().any(|s| requires_async_stmt(s)),
53        Stmt::Repeat { body, .. } => body.iter().any(|s| requires_async_stmt(s)),
54        Stmt::Zone { body, .. } => body.iter().any(|s| requires_async_stmt(s)),
55        Stmt::Parallel { tasks } => tasks.iter().any(|s| requires_async_stmt(s)),
56        Stmt::FunctionDef { body, .. } => body.iter().any(|s| requires_async_stmt(s)),
57        // Check Inspect arms for async operations
58        Stmt::Inspect { arms, .. } => {
59            arms.iter().any(|arm| arm.body.iter().any(|s| requires_async_stmt(s)))
60        }
61        _ => false,
62    }
63}
64
65/// Phase 53: Detect if any statements require VFS (Virtual File System).
66/// Returns true if the program uses file operations or persistent storage.
67pub(super) fn requires_vfs(stmts: &[Stmt]) -> bool {
68    stmts.iter().any(|s| requires_vfs_stmt(s))
69}
70
71pub(super) fn requires_vfs_stmt(stmt: &Stmt) -> bool {
72    match stmt {
73        // Phase 53: Mount uses VFS for persistent storage
74        Stmt::Mount { .. } => true,
75        // Phase 53: File I/O uses VFS
76        Stmt::ReadFrom { source: ReadSource::File(_), .. } => true,
77        Stmt::WriteFile { .. } => true,
78        // Recursively check nested blocks
79        Stmt::If { then_block, else_block, .. } => {
80            then_block.iter().any(|s| requires_vfs_stmt(s))
81                || else_block.map_or(false, |b| b.iter().any(|s| requires_vfs_stmt(s)))
82        }
83        Stmt::While { body, .. } => body.iter().any(|s| requires_vfs_stmt(s)),
84        Stmt::Repeat { body, .. } => body.iter().any(|s| requires_vfs_stmt(s)),
85        Stmt::Zone { body, .. } => body.iter().any(|s| requires_vfs_stmt(s)),
86        Stmt::Concurrent { tasks } => tasks.iter().any(|s| requires_vfs_stmt(s)),
87        Stmt::Parallel { tasks } => tasks.iter().any(|s| requires_vfs_stmt(s)),
88        Stmt::FunctionDef { body, .. } => body.iter().any(|s| requires_vfs_stmt(s)),
89        _ => false,
90    }
91}
92
93/// Phase 49b: Extract root identifier from expression for mutability analysis.
94/// Works with both simple identifiers and field accesses.
95pub(super) fn get_root_identifier_for_mutability(expr: &Expr) -> Option<Symbol> {
96    match expr {
97        Expr::Identifier(sym) => Some(*sym),
98        Expr::FieldAccess { object, .. } => get_root_identifier_for_mutability(object),
99        _ => None,
100    }
101}
102
103/// Grand Challenge: Collect all variables that need `let mut` in Rust.
104/// This includes:
105/// - Variables that are targets of `Set` statements (reassignment)
106/// - Variables that are targets of `Push` statements (mutation via push)
107/// - Variables that are targets of `Pop` statements (mutation via pop)
108pub(super) fn collect_mutable_vars(stmts: &[Stmt]) -> HashSet<Symbol> {
109    let mut targets = HashSet::new();
110    for stmt in stmts {
111        collect_mutable_vars_stmt(stmt, &mut targets);
112    }
113    targets
114}
115
116pub(super) fn collect_mutable_vars_stmt(stmt: &Stmt, targets: &mut HashSet<Symbol>) {
117    match stmt {
118        Stmt::Set { target, .. } => {
119            targets.insert(*target);
120        }
121        Stmt::Push { collection, .. } => {
122            // If collection is an identifier or field access, root needs to be mutable
123            if let Some(sym) = get_root_identifier_for_mutability(collection) {
124                targets.insert(sym);
125            }
126        }
127        Stmt::Pop { collection, .. } => {
128            // If collection is an identifier or field access, root needs to be mutable
129            if let Some(sym) = get_root_identifier_for_mutability(collection) {
130                targets.insert(sym);
131            }
132        }
133        Stmt::Add { collection, .. } => {
134            // If collection is an identifier (Set) or field access, root needs to be mutable
135            if let Some(sym) = get_root_identifier_for_mutability(collection) {
136                targets.insert(sym);
137            }
138        }
139        Stmt::Remove { collection, .. } => {
140            // If collection is an identifier (Set) or field access, root needs to be mutable
141            if let Some(sym) = get_root_identifier_for_mutability(collection) {
142                targets.insert(sym);
143            }
144        }
145        Stmt::SetIndex { collection, .. } => {
146            // If collection is an identifier or field access, root needs to be mutable
147            if let Some(sym) = get_root_identifier_for_mutability(collection) {
148                targets.insert(sym);
149            }
150        }
151        Stmt::If { then_block, else_block, .. } => {
152            for s in *then_block {
153                collect_mutable_vars_stmt(s, targets);
154            }
155            if let Some(else_stmts) = else_block {
156                for s in *else_stmts {
157                    collect_mutable_vars_stmt(s, targets);
158                }
159            }
160        }
161        Stmt::While { body, .. } => {
162            for s in *body {
163                collect_mutable_vars_stmt(s, targets);
164            }
165        }
166        Stmt::Repeat { body, .. } => {
167            for s in *body {
168                collect_mutable_vars_stmt(s, targets);
169            }
170        }
171        Stmt::Zone { body, .. } => {
172            for s in *body {
173                collect_mutable_vars_stmt(s, targets);
174            }
175        }
176        // Inspect (pattern match) arms may contain mutations
177        Stmt::Inspect { arms, .. } => {
178            for arm in arms.iter() {
179                for s in arm.body.iter() {
180                    collect_mutable_vars_stmt(s, targets);
181                }
182            }
183        }
184        // Phase 9: Structured Concurrency blocks
185        Stmt::Concurrent { tasks } | Stmt::Parallel { tasks } => {
186            for s in *tasks {
187                collect_mutable_vars_stmt(s, targets);
188            }
189        }
190        // Phase 49b: CRDT operations require mutable access
191        Stmt::IncreaseCrdt { object, .. } | Stmt::DecreaseCrdt { object, .. } => {
192            // Extract root variable from field access (e.g., g.score -> g)
193            if let Some(sym) = get_root_identifier_for_mutability(object) {
194                targets.insert(sym);
195            }
196        }
197        Stmt::AppendToSequence { sequence, .. } => {
198            if let Some(sym) = get_root_identifier_for_mutability(sequence) {
199                targets.insert(sym);
200            }
201        }
202        Stmt::ResolveConflict { object, .. } => {
203            if let Some(sym) = get_root_identifier_for_mutability(object) {
204                targets.insert(sym);
205            }
206        }
207        // Phase 49b: SetField on MVRegister/LWWRegister uses .set() which requires &mut self
208        Stmt::SetField { object, .. } => {
209            if let Some(sym) = get_root_identifier_for_mutability(object) {
210                targets.insert(sym);
211            }
212        }
213        _ => {}
214    }
215}
216
217/// Detect mutable Text variables that are only ever assigned single-character
218/// string literals. These can be emitted as `u8` (byte) instead of `String`,
219/// eliminating heap allocations for temporary character variables.
220///
221/// Returns a set of symbols that qualify for the u8 optimization.
222///
223/// A variable qualifies if:
224/// 1. It is declared with `Let mutable ch be "x"` (single-char text literal)
225/// 2. Every `Set ch to "y"` assigns a single-char text literal
226/// 3. It is never used in a context that requires String (e.g., function call args,
227///    Return, field access) — only push_str (→ push), Show, or comparison
228pub(super) fn collect_single_char_text_vars(stmts: &[Stmt], interner: &Interner) -> HashSet<Symbol> {
229    let mut candidates: HashSet<Symbol> = HashSet::new();
230    let mut disqualified: HashSet<Symbol> = HashSet::new();
231
232    // Pass 1: Find candidates (Let mutable with single-char text literal)
233    // and check all Set assignments are also single-char.
234    scan_single_char_candidates(stmts, interner, &mut candidates, &mut disqualified);
235
236    // Remove disqualified
237    for sym in &disqualified {
238        candidates.remove(sym);
239    }
240
241    // Pass 2: Check usage contexts. If any use requires a String, disqualify.
242    let mut usage_disqualified: HashSet<Symbol> = HashSet::new();
243    check_single_char_usage(stmts, &candidates, &mut usage_disqualified);
244
245    for sym in &usage_disqualified {
246        candidates.remove(sym);
247    }
248
249    candidates
250}
251
252fn is_single_char_text_literal<'a>(expr: &Expr<'a>, interner: &Interner) -> bool {
253    if let Expr::Literal(crate::ast::stmt::Literal::Text(sym)) = expr {
254        let text = interner.resolve(*sym);
255        text.len() == 1 && text.is_ascii()
256    } else {
257        false
258    }
259}
260
261fn scan_single_char_candidates(
262    stmts: &[Stmt],
263    interner: &Interner,
264    candidates: &mut HashSet<Symbol>,
265    disqualified: &mut HashSet<Symbol>,
266) {
267    for stmt in stmts {
268        match stmt {
269            Stmt::Let { var, value, mutable, .. } => {
270                if *mutable && is_single_char_text_literal(value, interner) {
271                    candidates.insert(*var);
272                }
273                // Recurse into nested blocks in any case
274            }
275            Stmt::Set { target, value } => {
276                if candidates.contains(target) || !disqualified.contains(target) {
277                    if !is_single_char_text_literal(value, interner) {
278                        // Non-single-char assignment disqualifies
279                        disqualified.insert(*target);
280                    }
281                }
282            }
283            Stmt::If { then_block, else_block, .. } => {
284                scan_single_char_candidates(then_block, interner, candidates, disqualified);
285                if let Some(eb) = else_block {
286                    scan_single_char_candidates(eb, interner, candidates, disqualified);
287                }
288            }
289            Stmt::While { body, .. } | Stmt::Repeat { body, .. } | Stmt::Zone { body, .. } => {
290                scan_single_char_candidates(body, interner, candidates, disqualified);
291            }
292            Stmt::FunctionDef { body, .. } => {
293                scan_single_char_candidates(body, interner, candidates, disqualified);
294            }
295            Stmt::Concurrent { tasks } | Stmt::Parallel { tasks } => {
296                scan_single_char_candidates(tasks, interner, candidates, disqualified);
297            }
298            _ => {}
299        }
300    }
301}
302
303/// Check that a candidate variable is only used in contexts compatible with u8:
304/// - `Set text to text + ch` (self-append → push)
305/// - `Show ch`
306/// - Comparison (`ch equals "x"`)
307///
308/// Disqualify if used in:
309/// - Function call arguments
310/// - Return value
311/// - Assignment to another variable (`Let y be ch`)
312/// - Any other expression context that expects String
313fn check_single_char_usage(
314    stmts: &[Stmt],
315    candidates: &HashSet<Symbol>,
316    disqualified: &mut HashSet<Symbol>,
317) {
318    for stmt in stmts {
319        match stmt {
320            Stmt::Set { target, value } => {
321                // `Set ch to "x"` is fine (already validated in pass 1).
322                // `Set text to text + ch` is fine (self-append).
323                // But if ch appears in a non-append context in value, disqualify.
324                if !candidates.contains(target) {
325                    // target is not a candidate, check if value uses a candidate
326                    // in a non-append-compatible way
327                    check_expr_usage(value, candidates, disqualified, true);
328                }
329            }
330            Stmt::Show { object, .. } => {
331                // Show ch is fine — we'll emit `println!("{}", ch as char)`
332                // But don't check deeper — identifiers in Show are OK
333            }
334            Stmt::Let { value, .. } => {
335                // `Let y be ch` would assign a u8 to y, which may not work.
336                // Disqualify if a candidate appears directly as the value.
337                check_expr_usage_strict(value, candidates, disqualified);
338            }
339            Stmt::Return { value: Some(v) } => {
340                check_expr_usage_strict(v, candidates, disqualified);
341            }
342            Stmt::Call { args, .. } => {
343                for a in args.iter() {
344                    check_expr_usage_strict(a, candidates, disqualified);
345                }
346            }
347            Stmt::Push { value, .. } => {
348                // `Push ch to list` — disqualify since list expects String
349                check_expr_usage_strict(value, candidates, disqualified);
350            }
351            Stmt::If { cond, then_block, else_block } => {
352                // Comparisons in conditions are fine for u8
353                check_single_char_usage(then_block, candidates, disqualified);
354                if let Some(eb) = else_block {
355                    check_single_char_usage(eb, candidates, disqualified);
356                }
357            }
358            Stmt::While { body, .. } | Stmt::Repeat { body, .. } | Stmt::Zone { body, .. } => {
359                check_single_char_usage(body, candidates, disqualified);
360            }
361            Stmt::FunctionDef { body, .. } => {
362                check_single_char_usage(body, candidates, disqualified);
363            }
364            _ => {}
365        }
366    }
367}
368
369/// Check an expression for usage of candidates in self-append context.
370/// `is_self_append` indicates the expression is the RHS of `Set text to <expr>`
371/// where text is a string (not a candidate). In that context, `text + ch` is fine.
372fn check_expr_usage(
373    expr: &Expr,
374    candidates: &HashSet<Symbol>,
375    disqualified: &mut HashSet<Symbol>,
376    is_self_append: bool,
377) {
378    match expr {
379        Expr::BinaryOp { op: crate::ast::stmt::BinaryOpKind::Add, left, right } if is_self_append => {
380            // In self-append: `text + ch` — ch as a direct identifier is fine
381            check_expr_usage(left, candidates, disqualified, true);
382            // right side: if it's a bare candidate identifier, that's fine (push)
383            if !matches!(right, Expr::Identifier(sym) if candidates.contains(sym)) {
384                check_expr_usage(right, candidates, disqualified, false);
385            }
386        }
387        Expr::Identifier(sym) if !is_self_append => {
388            // Bare candidate in non-append context — disqualify
389            if candidates.contains(sym) {
390                disqualified.insert(*sym);
391            }
392        }
393        _ => {}
394    }
395}
396
397/// Strictly disqualify any candidate that appears anywhere in this expression.
398fn check_expr_usage_strict(expr: &Expr, candidates: &HashSet<Symbol>, disqualified: &mut HashSet<Symbol>) {
399    match expr {
400        Expr::Identifier(sym) => {
401            if candidates.contains(sym) {
402                disqualified.insert(*sym);
403            }
404        }
405        Expr::BinaryOp { left, right, .. } => {
406            check_expr_usage_strict(left, candidates, disqualified);
407            check_expr_usage_strict(right, candidates, disqualified);
408        }
409        Expr::Call { args, .. } => {
410            for a in args.iter() {
411                check_expr_usage_strict(a, candidates, disqualified);
412            }
413        }
414        Expr::Not { operand } => check_expr_usage_strict(operand, candidates, disqualified),
415        Expr::Index { collection, index } => {
416            check_expr_usage_strict(collection, candidates, disqualified);
417            check_expr_usage_strict(index, candidates, disqualified);
418        }
419        Expr::Length { collection } => check_expr_usage_strict(collection, candidates, disqualified),
420        Expr::List(items) => {
421            for item in items.iter() {
422                check_expr_usage_strict(item, candidates, disqualified);
423            }
424        }
425        _ => {}
426    }
427}
428
429pub(super) fn collect_crdt_register_fields(registry: &TypeRegistry, interner: &Interner) -> (HashSet<(String, String)>, HashSet<(String, String)>) {
430    let mut lww_fields = HashSet::new();
431    let mut mv_fields = HashSet::new();
432    for (type_sym, def) in registry.iter_types() {
433        if let TypeDef::Struct { fields, .. } = def {
434            let type_name = interner.resolve(*type_sym).to_string();
435            for field in fields {
436                if let FieldType::Generic { base, .. } = &field.ty {
437                    let base_name = interner.resolve(*base);
438                    let field_name = interner.resolve(field.name).to_string();
439                    if base_name == "LastWriteWins" {
440                        lww_fields.insert((type_name.clone(), field_name));
441                    } else if base_name == "Divergent" || base_name == "MVRegister" {
442                        mv_fields.insert((type_name.clone(), field_name));
443                    }
444                }
445            }
446        }
447    }
448    (lww_fields, mv_fields)
449}
450
451/// Phase 102: Collect enum fields that need Box<T> for recursion.
452/// Returns a set of (EnumName, VariantName, FieldName) tuples.
453pub(super) fn collect_boxed_fields(registry: &TypeRegistry, interner: &Interner) -> HashSet<(String, String, String)> {
454    let mut boxed_fields = HashSet::new();
455    for (type_sym, def) in registry.iter_types() {
456        if let TypeDef::Enum { variants, .. } = def {
457            let enum_name = interner.resolve(*type_sym);
458            for variant in variants {
459                let variant_name = interner.resolve(variant.name);
460                for field in &variant.fields {
461                    if is_recursive_field(&field.ty, enum_name, interner) {
462                        let field_name = interner.resolve(field.name).to_string();
463                        boxed_fields.insert((
464                            enum_name.to_string(),
465                            variant_name.to_string(),
466                            field_name,
467                        ));
468                    }
469                }
470            }
471        }
472    }
473    boxed_fields
474}
475
476/// Phase 54: Collect function names that are async.
477/// Used by LaunchTask codegen to determine if .await is needed.
478///
479/// Two-pass analysis:
480/// 1. First pass: Collect directly async functions (have Sleep, LaunchTask, etc.)
481/// 2. Second pass: Iterate until fixed point - if function calls an async function, mark it async
482pub fn collect_async_functions(stmts: &[Stmt]) -> HashSet<Symbol> {
483    // First, collect all function definitions
484    let mut func_bodies: HashMap<Symbol, &[Stmt]> = HashMap::new();
485    for stmt in stmts {
486        if let Stmt::FunctionDef { name, body, .. } = stmt {
487            func_bodies.insert(*name, *body);
488        }
489    }
490
491    // Pass 1: Collect directly async functions
492    let mut async_fns = HashSet::new();
493    for stmt in stmts {
494        if let Stmt::FunctionDef { name, body, .. } = stmt {
495            if body.iter().any(|s| requires_async_stmt(s)) {
496                async_fns.insert(*name);
497            }
498        }
499    }
500
501    // Pass 2: Propagate async-ness through call graph until fixed point
502    loop {
503        let mut changed = false;
504        for (func_name, body) in &func_bodies {
505            if async_fns.contains(func_name) {
506                continue; // Already marked async
507            }
508            // Check if this function calls any async function
509            if body.iter().any(|s| calls_async_function(s, &async_fns)) {
510                async_fns.insert(*func_name);
511                changed = true;
512            }
513        }
514        if !changed {
515            break;
516        }
517    }
518
519    async_fns
520}
521
522/// Helper: Check if a statement calls any function in the async_fns set
523pub(super) fn calls_async_function(stmt: &Stmt, async_fns: &HashSet<Symbol>) -> bool {
524    match stmt {
525        Stmt::Call { function, args } => {
526            // Check if the called function is async OR if any argument expression calls an async function
527            async_fns.contains(function)
528                || args.iter().any(|a| calls_async_function_in_expr(a, async_fns))
529        }
530        Stmt::If { cond, then_block, else_block } => {
531            calls_async_function_in_expr(cond, async_fns)
532                || then_block.iter().any(|s| calls_async_function(s, async_fns))
533                || else_block.map_or(false, |b| b.iter().any(|s| calls_async_function(s, async_fns)))
534        }
535        Stmt::While { cond, body, .. } => {
536            calls_async_function_in_expr(cond, async_fns)
537                || body.iter().any(|s| calls_async_function(s, async_fns))
538        }
539        Stmt::Repeat { iterable, body, .. } => {
540            calls_async_function_in_expr(iterable, async_fns)
541                || body.iter().any(|s| calls_async_function(s, async_fns))
542        }
543        Stmt::Zone { body, .. } => {
544            body.iter().any(|s| calls_async_function(s, async_fns))
545        }
546        Stmt::Concurrent { tasks } | Stmt::Parallel { tasks } => {
547            tasks.iter().any(|s| calls_async_function(s, async_fns))
548        }
549        Stmt::FunctionDef { body, .. } => {
550            body.iter().any(|s| calls_async_function(s, async_fns))
551        }
552        // Check Let statements for async function calls in the value expression
553        Stmt::Let { value, .. } => calls_async_function_in_expr(value, async_fns),
554        // Check Set statements for async function calls in the value expression
555        Stmt::Set { value, .. } => calls_async_function_in_expr(value, async_fns),
556        // Check Return statements for async function calls in the return value
557        Stmt::Return { value } => {
558            value.as_ref().map_or(false, |v| calls_async_function_in_expr(v, async_fns))
559        }
560        // Check RuntimeAssert condition for async calls
561        Stmt::RuntimeAssert { condition, .. } => calls_async_function_in_expr(condition, async_fns),
562        // Check Show for async calls
563        Stmt::Show { object, .. } => calls_async_function_in_expr(object, async_fns),
564        // Check Push for async calls
565        Stmt::Push { collection, value } => {
566            calls_async_function_in_expr(collection, async_fns)
567                || calls_async_function_in_expr(value, async_fns)
568        }
569        // Check SetIndex for async calls
570        Stmt::SetIndex { collection, index, value } => {
571            calls_async_function_in_expr(collection, async_fns)
572                || calls_async_function_in_expr(index, async_fns)
573                || calls_async_function_in_expr(value, async_fns)
574        }
575        // Check SendPipe for async calls
576        Stmt::SendPipe { value, pipe } | Stmt::TrySendPipe { value, pipe, .. } => {
577            calls_async_function_in_expr(value, async_fns)
578                || calls_async_function_in_expr(pipe, async_fns)
579        }
580        // Check Inspect arms for async function calls
581        Stmt::Inspect { target, arms, .. } => {
582            calls_async_function_in_expr(target, async_fns)
583                || arms.iter().any(|arm| arm.body.iter().any(|s| calls_async_function(s, async_fns)))
584        }
585        _ => false,
586    }
587}
588
589/// Helper: Check if an expression calls any function in the async_fns set
590fn calls_async_function_in_expr(expr: &Expr, async_fns: &HashSet<Symbol>) -> bool {
591    match expr {
592        Expr::Call { function, args } => {
593            async_fns.contains(function)
594                || args.iter().any(|a| calls_async_function_in_expr(a, async_fns))
595        }
596        Expr::BinaryOp { left, right, .. } => {
597            calls_async_function_in_expr(left, async_fns)
598                || calls_async_function_in_expr(right, async_fns)
599        }
600        Expr::Index { collection, index } => {
601            calls_async_function_in_expr(collection, async_fns)
602                || calls_async_function_in_expr(index, async_fns)
603        }
604        Expr::FieldAccess { object, .. } => calls_async_function_in_expr(object, async_fns),
605        Expr::List(items) | Expr::Tuple(items) => {
606            items.iter().any(|i| calls_async_function_in_expr(i, async_fns))
607        }
608        Expr::Closure { body, .. } => {
609            match body {
610                crate::ast::stmt::ClosureBody::Expression(expr) => calls_async_function_in_expr(expr, async_fns),
611                crate::ast::stmt::ClosureBody::Block(_) => false,
612            }
613        }
614        Expr::CallExpr { callee, args } => {
615            calls_async_function_in_expr(callee, async_fns)
616                || args.iter().any(|a| calls_async_function_in_expr(a, async_fns))
617        }
618        Expr::InterpolatedString(parts) => {
619            parts.iter().any(|p| {
620                if let crate::ast::stmt::StringPart::Expr { value, .. } = p {
621                    calls_async_function_in_expr(value, async_fns)
622                } else { false }
623            })
624        }
625        _ => false,
626    }
627}
628
629// =============================================================================
630// Purity Analysis
631// =============================================================================
632
633pub(super) fn collect_pure_functions(stmts: &[Stmt]) -> HashSet<Symbol> {
634    let mut func_bodies: HashMap<Symbol, &[Stmt]> = HashMap::new();
635    for stmt in stmts {
636        if let Stmt::FunctionDef { name, body, .. } = stmt {
637            func_bodies.insert(*name, *body);
638        }
639    }
640
641    // Pass 1: Mark functions as impure if they directly contain impure statements
642    let mut impure_fns = HashSet::new();
643    for (func_name, body) in &func_bodies {
644        if body.iter().any(|s| is_directly_impure_stmt(s)) {
645            impure_fns.insert(*func_name);
646        }
647    }
648
649    // Pass 2: Propagate impurity through call graph until fixed point
650    loop {
651        let mut changed = false;
652        for (func_name, body) in &func_bodies {
653            if impure_fns.contains(func_name) {
654                continue;
655            }
656            if body.iter().any(|s| calls_impure_function(s, &impure_fns)) {
657                impure_fns.insert(*func_name);
658                changed = true;
659            }
660        }
661        if !changed {
662            break;
663        }
664    }
665
666    // Pure = all functions NOT in impure set
667    let mut pure_fns = HashSet::new();
668    for func_name in func_bodies.keys() {
669        if !impure_fns.contains(func_name) {
670            pure_fns.insert(*func_name);
671        }
672    }
673    pure_fns
674}
675
676fn is_directly_impure_stmt(stmt: &Stmt) -> bool {
677    match stmt {
678        Stmt::Show { .. }
679        | Stmt::Give { .. }
680        | Stmt::WriteFile { .. }
681        | Stmt::ReadFrom { .. }
682        | Stmt::Listen { .. }
683        | Stmt::ConnectTo { .. }
684        | Stmt::SendMessage { .. }
685        | Stmt::StreamMessage { .. }
686        | Stmt::AwaitMessage { .. }
687        | Stmt::Sleep { .. }
688        | Stmt::Sync { .. }
689        | Stmt::Mount { .. }
690        | Stmt::MergeCrdt { .. }
691        | Stmt::IncreaseCrdt { .. }
692        | Stmt::DecreaseCrdt { .. }
693        | Stmt::AppendToSequence { .. }
694        | Stmt::ResolveConflict { .. }
695        | Stmt::CreatePipe { .. }
696        | Stmt::SendPipe { .. }
697        | Stmt::ReceivePipe { .. }
698        | Stmt::TrySendPipe { .. }
699        | Stmt::TryReceivePipe { .. }
700        | Stmt::LaunchTask { .. }
701        | Stmt::LaunchTaskWithHandle { .. }
702        | Stmt::StopTask { .. }
703        | Stmt::Concurrent { .. }
704        | Stmt::Parallel { .. } => true,
705        Stmt::If { then_block, else_block, .. } => {
706            then_block.iter().any(|s| is_directly_impure_stmt(s))
707                || else_block.map_or(false, |b| b.iter().any(|s| is_directly_impure_stmt(s)))
708        }
709        Stmt::While { body, .. } | Stmt::Repeat { body, .. } => {
710            body.iter().any(|s| is_directly_impure_stmt(s))
711        }
712        Stmt::Zone { body, .. } => {
713            body.iter().any(|s| is_directly_impure_stmt(s))
714        }
715        Stmt::Inspect { arms, .. } => {
716            arms.iter().any(|arm| arm.body.iter().any(|s| is_directly_impure_stmt(s)))
717        }
718        _ => false,
719    }
720}
721
722fn calls_impure_function(stmt: &Stmt, impure_fns: &HashSet<Symbol>) -> bool {
723    match stmt {
724        Stmt::Call { function, args } => {
725            impure_fns.contains(function)
726                || args.iter().any(|a| expr_calls_impure(a, impure_fns))
727        }
728        Stmt::Let { value, .. } => expr_calls_impure(value, impure_fns),
729        Stmt::Set { value, .. } => expr_calls_impure(value, impure_fns),
730        Stmt::Return { value } => value.as_ref().map_or(false, |v| expr_calls_impure(v, impure_fns)),
731        Stmt::If { cond, then_block, else_block } => {
732            expr_calls_impure(cond, impure_fns)
733                || then_block.iter().any(|s| calls_impure_function(s, impure_fns))
734                || else_block.map_or(false, |b| b.iter().any(|s| calls_impure_function(s, impure_fns)))
735        }
736        Stmt::While { cond, body, .. } => {
737            expr_calls_impure(cond, impure_fns)
738                || body.iter().any(|s| calls_impure_function(s, impure_fns))
739        }
740        Stmt::Repeat { body, .. } => body.iter().any(|s| calls_impure_function(s, impure_fns)),
741        Stmt::Zone { body, .. } => body.iter().any(|s| calls_impure_function(s, impure_fns)),
742        Stmt::Inspect { arms, .. } => {
743            arms.iter().any(|arm| arm.body.iter().any(|s| calls_impure_function(s, impure_fns)))
744        }
745        Stmt::Show { object, .. } => expr_calls_impure(object, impure_fns),
746        Stmt::Push { value, collection } | Stmt::Add { value, collection } | Stmt::Remove { value, collection } => {
747            expr_calls_impure(value, impure_fns) || expr_calls_impure(collection, impure_fns)
748        }
749        _ => false,
750    }
751}
752
753fn expr_calls_impure(expr: &Expr, impure_fns: &HashSet<Symbol>) -> bool {
754    match expr {
755        Expr::Call { function, args } => {
756            impure_fns.contains(function)
757                || args.iter().any(|a| expr_calls_impure(a, impure_fns))
758        }
759        Expr::BinaryOp { left, right, .. } => {
760            expr_calls_impure(left, impure_fns) || expr_calls_impure(right, impure_fns)
761        }
762        Expr::Index { collection, index } => {
763            expr_calls_impure(collection, impure_fns) || expr_calls_impure(index, impure_fns)
764        }
765        Expr::FieldAccess { object, .. } => expr_calls_impure(object, impure_fns),
766        Expr::List(items) | Expr::Tuple(items) => items.iter().any(|i| expr_calls_impure(i, impure_fns)),
767        Expr::CallExpr { callee, args } => {
768            expr_calls_impure(callee, impure_fns)
769                || args.iter().any(|a| expr_calls_impure(a, impure_fns))
770        }
771        Expr::InterpolatedString(parts) => {
772            parts.iter().any(|p| {
773                if let crate::ast::stmt::StringPart::Expr { value, .. } = p {
774                    expr_calls_impure(value, impure_fns)
775                } else { false }
776            })
777        }
778        _ => false,
779    }
780}
781
782// =============================================================================
783// Memoization Detection
784// =============================================================================
785
786pub(super) fn count_self_calls(func_name: Symbol, body: &[Stmt]) -> usize {
787    let mut count = 0;
788    for stmt in body {
789        count += count_self_calls_in_stmt(func_name, stmt);
790    }
791    count
792}
793
794fn count_self_calls_in_stmt(func_name: Symbol, stmt: &Stmt) -> usize {
795    match stmt {
796        Stmt::Return { value: Some(expr) } => count_self_calls_in_expr(func_name, expr),
797        Stmt::Let { value, .. } => count_self_calls_in_expr(func_name, value),
798        Stmt::Set { value, .. } => count_self_calls_in_expr(func_name, value),
799        Stmt::Call { function, args } => {
800            let mut c = if *function == func_name { 1 } else { 0 };
801            c += args.iter().map(|a| count_self_calls_in_expr(func_name, a)).sum::<usize>();
802            c
803        }
804        Stmt::If { cond, then_block, else_block } => {
805            let mut c = count_self_calls_in_expr(func_name, cond);
806            c += count_self_calls(func_name, then_block);
807            if let Some(else_stmts) = else_block {
808                c += count_self_calls(func_name, else_stmts);
809            }
810            c
811        }
812        Stmt::While { cond, body, .. } => {
813            count_self_calls_in_expr(func_name, cond) + count_self_calls(func_name, body)
814        }
815        Stmt::Repeat { body, .. } => count_self_calls(func_name, body),
816        Stmt::Show { object, .. } => count_self_calls_in_expr(func_name, object),
817        _ => 0,
818    }
819}
820
821fn count_self_calls_in_expr(func_name: Symbol, expr: &Expr) -> usize {
822    match expr {
823        Expr::Call { function, args } => {
824            let mut c = if *function == func_name { 1 } else { 0 };
825            c += args.iter().map(|a| count_self_calls_in_expr(func_name, a)).sum::<usize>();
826            c
827        }
828        Expr::BinaryOp { left, right, .. } => {
829            count_self_calls_in_expr(func_name, left) + count_self_calls_in_expr(func_name, right)
830        }
831        Expr::Index { collection, index } => {
832            count_self_calls_in_expr(func_name, collection) + count_self_calls_in_expr(func_name, index)
833        }
834        Expr::FieldAccess { object, .. } => count_self_calls_in_expr(func_name, object),
835        Expr::List(items) | Expr::Tuple(items) => {
836            items.iter().map(|i| count_self_calls_in_expr(func_name, i)).sum()
837        }
838        Expr::InterpolatedString(parts) => {
839            parts.iter().map(|p| {
840                if let crate::ast::stmt::StringPart::Expr { value, .. } = p {
841                    count_self_calls_in_expr(func_name, value)
842                } else { 0 }
843            }).sum()
844        }
845        _ => 0,
846    }
847}
848
849pub(super) fn is_hashable_type(ty: &TypeExpr, interner: &Interner) -> bool {
850    match ty {
851        TypeExpr::Primitive(sym) | TypeExpr::Named(sym) => {
852            let name = interner.resolve(*sym);
853            matches!(name, "Int" | "Nat" | "Bool" | "Char" | "Byte" | "Text"
854                | "i64" | "u64" | "bool" | "char" | "u8" | "String")
855        }
856        TypeExpr::Refinement { base, .. } => is_hashable_type(base, interner),
857        _ => false,
858    }
859}
860
861pub(super) fn is_copy_type_expr(ty: &TypeExpr, interner: &Interner) -> bool {
862    match ty {
863        TypeExpr::Primitive(sym) | TypeExpr::Named(sym) => {
864            let name = interner.resolve(*sym);
865            matches!(name, "Int" | "Nat" | "Bool" | "Char" | "Byte"
866                | "i64" | "u64" | "bool" | "char" | "u8")
867        }
868        TypeExpr::Refinement { base, .. } => is_copy_type_expr(base, interner),
869        _ => false,
870    }
871}
872
873pub(super) fn should_memoize(
874    name: Symbol,
875    body: &[Stmt],
876    params: &[(Symbol, &TypeExpr)],
877    return_type: Option<&TypeExpr>,
878    is_pure: bool,
879    interner: &Interner,
880) -> bool {
881    if !is_pure {
882        return false;
883    }
884    if !body_contains_self_call(name, body) {
885        return false;
886    }
887    if count_self_calls(name, body) < 2 {
888        return false;
889    }
890    if params.is_empty() {
891        return false;
892    }
893    if !params.iter().all(|(_, ty)| is_hashable_type(ty, interner)) {
894        return false;
895    }
896    if return_type.is_none() {
897        return false;
898    }
899    true
900}
901
902pub(super) fn body_contains_self_call(func_name: Symbol, body: &[Stmt]) -> bool {
903    body.iter().any(|s| stmt_contains_self_call(func_name, s))
904}
905
906fn stmt_contains_self_call(func_name: Symbol, stmt: &Stmt) -> bool {
907    match stmt {
908        Stmt::Return { value: Some(expr) } => expr_contains_self_call(func_name, expr),
909        Stmt::Return { value: None } => false,
910        Stmt::Let { value, .. } => expr_contains_self_call(func_name, value),
911        Stmt::Set { value, .. } => expr_contains_self_call(func_name, value),
912        Stmt::Call { function, args } => {
913            *function == func_name || args.iter().any(|a| expr_contains_self_call(func_name, a))
914        }
915        Stmt::If { cond, then_block, else_block } => {
916            expr_contains_self_call(func_name, cond)
917                || then_block.iter().any(|s| stmt_contains_self_call(func_name, s))
918                || else_block.map_or(false, |b| b.iter().any(|s| stmt_contains_self_call(func_name, s)))
919        }
920        Stmt::While { cond, body, .. } => {
921            expr_contains_self_call(func_name, cond)
922                || body.iter().any(|s| stmt_contains_self_call(func_name, s))
923        }
924        Stmt::Repeat { body, .. } => {
925            body.iter().any(|s| stmt_contains_self_call(func_name, s))
926        }
927        Stmt::Show { object, .. } => expr_contains_self_call(func_name, object),
928        _ => false,
929    }
930}
931
932pub(super) fn expr_contains_self_call(func_name: Symbol, expr: &Expr) -> bool {
933    match expr {
934        Expr::Call { function, args } => {
935            *function == func_name || args.iter().any(|a| expr_contains_self_call(func_name, a))
936        }
937        Expr::BinaryOp { left, right, .. } => {
938            expr_contains_self_call(func_name, left) || expr_contains_self_call(func_name, right)
939        }
940        Expr::Index { collection, index } => {
941            expr_contains_self_call(func_name, collection) || expr_contains_self_call(func_name, index)
942        }
943        Expr::FieldAccess { object, .. } => expr_contains_self_call(func_name, object),
944        Expr::List(items) | Expr::Tuple(items) => {
945            items.iter().any(|i| expr_contains_self_call(func_name, i))
946        }
947        Expr::InterpolatedString(parts) => {
948            parts.iter().any(|p| {
949                if let crate::ast::stmt::StringPart::Expr { value, .. } = p {
950                    expr_contains_self_call(func_name, value)
951                } else { false }
952            })
953        }
954        _ => false,
955    }
956}
957
958/// Check if an expression contains an `Expr::Index` that reads from the given collection symbol.
959/// Used by SetIndex to decide whether the value expression aliases the collection being written to,
960/// which requires a temporary binding to avoid borrow conflicts.
961pub(super) fn expr_indexes_collection(expr: &Expr, coll_sym: Symbol) -> bool {
962    match expr {
963        Expr::Index { collection, index } => {
964            let indexes_this = matches!(collection, Expr::Identifier(sym) if *sym == coll_sym);
965            indexes_this
966                || expr_indexes_collection(collection, coll_sym)
967                || expr_indexes_collection(index, coll_sym)
968        }
969        Expr::BinaryOp { left, right, .. } => {
970            expr_indexes_collection(left, coll_sym) || expr_indexes_collection(right, coll_sym)
971        }
972        Expr::Call { args, .. } => {
973            args.iter().any(|a| expr_indexes_collection(a, coll_sym))
974        }
975        Expr::FieldAccess { object, .. } => expr_indexes_collection(object, coll_sym),
976        Expr::Length { collection } => expr_indexes_collection(collection, coll_sym),
977        Expr::Not { operand } => expr_indexes_collection(operand, coll_sym),
978        Expr::List(items) | Expr::Tuple(items) => {
979            items.iter().any(|i| expr_indexes_collection(i, coll_sym))
980        }
981        _ => false,
982    }
983}
984
985/// True if `expr` borrows ANY collection at runtime — an index (`item i of C`),
986/// slice, or length read. Under LOGOS reference semantics any such collection
987/// may alias the target of a `SetIndex`, so the RHS must be evaluated into a
988/// temp BEFORE the target's `borrow_mut()` is taken; otherwise the live
989/// `borrow()` and the `borrow_mut()` can land on the same `RefCell` and panic
990/// ("already borrowed"). `expr_indexes_collection` only catches same-symbol
991/// aliasing; this catches the cross-variable case (e.g. `prev` aliasing `curr`
992/// after `Set prev to curr`).
993pub(super) fn expr_reads_any_collection(expr: &Expr) -> bool {
994    match expr {
995        Expr::Index { .. } | Expr::Slice { .. } | Expr::Length { .. } => true,
996        Expr::BinaryOp { left, right, .. } => {
997            expr_reads_any_collection(left) || expr_reads_any_collection(right)
998        }
999        Expr::Call { args, .. } => args.iter().any(|a| expr_reads_any_collection(a)),
1000        Expr::FieldAccess { object, .. } => expr_reads_any_collection(object),
1001        Expr::Not { operand } => expr_reads_any_collection(operand),
1002        Expr::List(items) | Expr::Tuple(items) => items.iter().any(|i| expr_reads_any_collection(i)),
1003        _ => false,
1004    }
1005}
1006
1007// =============================================================================
1008// Inline Annotation Detection
1009// =============================================================================
1010
1011pub(super) fn should_inline(name: Symbol, body: &[Stmt], is_native: bool, is_exported: bool, is_async: bool) -> bool {
1012    !is_native && !is_exported && !is_async
1013        && body.len() <= 10
1014        && !body_contains_self_call(name, body)
1015}
1016
1017// =============================================================================
1018// Pipe Detection
1019// =============================================================================
1020
1021pub fn collect_pipe_sender_params(body: &[Stmt]) -> HashSet<Symbol> {
1022    let mut senders = HashSet::new();
1023    for stmt in body {
1024        collect_pipe_sender_params_stmt(stmt, &mut senders);
1025    }
1026    senders
1027}
1028
1029fn collect_pipe_sender_params_stmt(stmt: &Stmt, senders: &mut HashSet<Symbol>) {
1030    match stmt {
1031        Stmt::SendPipe { pipe, .. } | Stmt::TrySendPipe { pipe, .. } => {
1032            if let Expr::Identifier(sym) = pipe {
1033                senders.insert(*sym);
1034            }
1035        }
1036        Stmt::If { then_block, else_block, .. } => {
1037            for s in *then_block {
1038                collect_pipe_sender_params_stmt(s, senders);
1039            }
1040            if let Some(else_stmts) = else_block {
1041                for s in *else_stmts {
1042                    collect_pipe_sender_params_stmt(s, senders);
1043                }
1044            }
1045        }
1046        Stmt::While { body, .. } | Stmt::Repeat { body, .. } | Stmt::Zone { body, .. } => {
1047            for s in *body {
1048                collect_pipe_sender_params_stmt(s, senders);
1049            }
1050        }
1051        Stmt::Concurrent { tasks } | Stmt::Parallel { tasks } => {
1052            for s in *tasks {
1053                collect_pipe_sender_params_stmt(s, senders);
1054            }
1055        }
1056        _ => {}
1057    }
1058}
1059
1060/// Phase 54: Collect variables that are pipe declarations (created with CreatePipe).
1061/// These have _tx/_rx suffixes, while pipe parameters don't.
1062pub fn collect_pipe_vars(stmts: &[Stmt]) -> HashSet<Symbol> {
1063    let mut pipe_vars = HashSet::new();
1064    for stmt in stmts {
1065        collect_pipe_vars_stmt(stmt, &mut pipe_vars);
1066    }
1067    pipe_vars
1068}
1069
1070fn collect_pipe_vars_stmt(stmt: &Stmt, pipe_vars: &mut HashSet<Symbol>) {
1071    match stmt {
1072        Stmt::CreatePipe { var, .. } => {
1073            pipe_vars.insert(*var);
1074        }
1075        Stmt::If { then_block, else_block, .. } => {
1076            for s in *then_block {
1077                collect_pipe_vars_stmt(s, pipe_vars);
1078            }
1079            if let Some(else_stmts) = else_block {
1080                for s in *else_stmts {
1081                    collect_pipe_vars_stmt(s, pipe_vars);
1082                }
1083            }
1084        }
1085        Stmt::While { body, .. } | Stmt::Repeat { body, .. } | Stmt::Zone { body, .. } => {
1086            for s in *body {
1087                collect_pipe_vars_stmt(s, pipe_vars);
1088            }
1089        }
1090        Stmt::Concurrent { tasks } | Stmt::Parallel { tasks } => {
1091            for s in *tasks {
1092                collect_pipe_vars_stmt(s, pipe_vars);
1093            }
1094        }
1095        _ => {}
1096    }
1097}
1098
1099/// Collect all identifier symbols from an expression recursively.
1100/// Used by Concurrent/Parallel codegen to find variables that need cloning.
1101pub(super) fn collect_expr_identifiers(expr: &Expr, identifiers: &mut HashSet<Symbol>) {
1102    match expr {
1103        Expr::Identifier(sym) => {
1104            identifiers.insert(*sym);
1105        }
1106        Expr::BinaryOp { left, right, .. } => {
1107            collect_expr_identifiers(left, identifiers);
1108            collect_expr_identifiers(right, identifiers);
1109        }
1110        Expr::Call { args, .. } => {
1111            for arg in args {
1112                collect_expr_identifiers(arg, identifiers);
1113            }
1114        }
1115        Expr::Index { collection, index } => {
1116            collect_expr_identifiers(collection, identifiers);
1117            collect_expr_identifiers(index, identifiers);
1118        }
1119        Expr::Slice { collection, start, end } => {
1120            collect_expr_identifiers(collection, identifiers);
1121            collect_expr_identifiers(start, identifiers);
1122            collect_expr_identifiers(end, identifiers);
1123        }
1124        Expr::Copy { expr: inner } | Expr::Give { value: inner } | Expr::Length { collection: inner }
1125        | Expr::Not { operand: inner } => {
1126            collect_expr_identifiers(inner, identifiers);
1127        }
1128        Expr::Contains { collection, value } | Expr::Union { left: collection, right: value } | Expr::Intersection { left: collection, right: value } => {
1129            collect_expr_identifiers(collection, identifiers);
1130            collect_expr_identifiers(value, identifiers);
1131        }
1132        Expr::ManifestOf { zone } | Expr::ChunkAt { zone, .. } => {
1133            collect_expr_identifiers(zone, identifiers);
1134        }
1135        Expr::List(items) | Expr::Tuple(items) => {
1136            for item in items {
1137                collect_expr_identifiers(item, identifiers);
1138            }
1139        }
1140        Expr::Range { start, end } => {
1141            collect_expr_identifiers(start, identifiers);
1142            collect_expr_identifiers(end, identifiers);
1143        }
1144        Expr::FieldAccess { object, .. } => {
1145            collect_expr_identifiers(object, identifiers);
1146        }
1147        Expr::New { init_fields, .. } => {
1148            for (_, value) in init_fields {
1149                collect_expr_identifiers(value, identifiers);
1150            }
1151        }
1152        Expr::NewVariant { fields, .. } => {
1153            for (_, value) in fields {
1154                collect_expr_identifiers(value, identifiers);
1155            }
1156        }
1157        Expr::OptionSome { value } => {
1158            collect_expr_identifiers(value, identifiers);
1159        }
1160        Expr::WithCapacity { value, capacity } => {
1161            collect_expr_identifiers(value, identifiers);
1162            collect_expr_identifiers(capacity, identifiers);
1163        }
1164        Expr::Closure { body, .. } => {
1165            match body {
1166                crate::ast::stmt::ClosureBody::Expression(expr) => collect_expr_identifiers(expr, identifiers),
1167                crate::ast::stmt::ClosureBody::Block(_) => {}
1168            }
1169        }
1170        Expr::CallExpr { callee, args } => {
1171            collect_expr_identifiers(callee, identifiers);
1172            for arg in args {
1173                collect_expr_identifiers(arg, identifiers);
1174            }
1175        }
1176        Expr::InterpolatedString(parts) => {
1177            for part in parts {
1178                if let crate::ast::stmt::StringPart::Expr { value, .. } = part {
1179                    collect_expr_identifiers(value, identifiers);
1180                }
1181            }
1182        }
1183        Expr::OptionNone => {}
1184        Expr::Escape { .. } => {}
1185        Expr::Literal(_) => {}
1186    }
1187}
1188
1189/// Check if a symbol appears anywhere in a slice of statements (expressions or targets).
1190/// Used by dead-counter elimination to decide if a post-loop counter binding is needed.
1191pub(super) fn symbol_appears_in_stmts(sym: Symbol, stmts: &[&Stmt]) -> bool {
1192    stmts.iter().any(|s| symbol_appears_in_stmt(sym, s))
1193}
1194
1195pub(super) fn symbol_appears_in_stmt(sym: Symbol, stmt: &Stmt) -> bool {
1196    match stmt {
1197        Stmt::Let { value, .. } => symbol_appears_in_expr(sym, value),
1198        Stmt::Set { target, value, .. } => *target == sym || symbol_appears_in_expr(sym, value),
1199        Stmt::Show { object, .. } => symbol_appears_in_expr(sym, object),
1200        Stmt::Return { value } => value.as_ref().map_or(false, |v| symbol_appears_in_expr(sym, v)),
1201        Stmt::Call { function, args } => *function == sym || args.iter().any(|a| symbol_appears_in_expr(sym, a)),
1202        Stmt::If { cond, then_block, else_block } => {
1203            symbol_appears_in_expr(sym, cond)
1204                || then_block.iter().any(|s| symbol_appears_in_stmt(sym, s))
1205                || else_block.map_or(false, |b| b.iter().any(|s| symbol_appears_in_stmt(sym, s)))
1206        }
1207        Stmt::While { cond, body, .. } => {
1208            symbol_appears_in_expr(sym, cond)
1209                || body.iter().any(|s| symbol_appears_in_stmt(sym, s))
1210        }
1211        Stmt::Repeat { iterable, body, .. } => {
1212            symbol_appears_in_expr(sym, iterable)
1213                || body.iter().any(|s| symbol_appears_in_stmt(sym, s))
1214        }
1215        Stmt::Zone { body, .. } => body.iter().any(|s| symbol_appears_in_stmt(sym, s)),
1216        Stmt::Push { collection, value } => {
1217            symbol_appears_in_expr(sym, collection) || symbol_appears_in_expr(sym, value)
1218        }
1219        Stmt::Pop { collection, .. } => symbol_appears_in_expr(sym, collection),
1220        Stmt::Add { collection, value } | Stmt::Remove { collection, value } => {
1221            symbol_appears_in_expr(sym, collection) || symbol_appears_in_expr(sym, value)
1222        }
1223        Stmt::SetIndex { collection, index, value } => {
1224            symbol_appears_in_expr(sym, collection) || symbol_appears_in_expr(sym, index) || symbol_appears_in_expr(sym, value)
1225        }
1226        Stmt::Inspect { target, arms, .. } => {
1227            symbol_appears_in_expr(sym, target)
1228                || arms.iter().any(|arm| arm.body.iter().any(|s| symbol_appears_in_stmt(sym, s)))
1229        }
1230        Stmt::RuntimeAssert { condition, .. } => symbol_appears_in_expr(sym, condition),
1231        Stmt::FunctionDef { body, .. } => body.iter().any(|s| symbol_appears_in_stmt(sym, s)),
1232        Stmt::Concurrent { tasks } | Stmt::Parallel { tasks } => {
1233            tasks.iter().any(|s| symbol_appears_in_stmt(sym, s))
1234        }
1235        _ => false,
1236    }
1237}
1238
1239pub(super) fn symbol_appears_in_expr(sym: Symbol, expr: &Expr) -> bool {
1240    match expr {
1241        Expr::Identifier(s) => *s == sym,
1242        Expr::BinaryOp { left, right, .. } => {
1243            symbol_appears_in_expr(sym, left) || symbol_appears_in_expr(sym, right)
1244        }
1245        Expr::Call { function, args } => {
1246            *function == sym || args.iter().any(|a| symbol_appears_in_expr(sym, a))
1247        }
1248        Expr::Index { collection, index } => {
1249            symbol_appears_in_expr(sym, collection) || symbol_appears_in_expr(sym, index)
1250        }
1251        Expr::Slice { collection, start, end } => {
1252            symbol_appears_in_expr(sym, collection)
1253                || symbol_appears_in_expr(sym, start)
1254                || symbol_appears_in_expr(sym, end)
1255        }
1256        Expr::Length { collection } | Expr::Copy { expr: collection } | Expr::Give { value: collection } => {
1257            symbol_appears_in_expr(sym, collection)
1258        }
1259        Expr::Contains { collection, value } | Expr::Union { left: collection, right: value } | Expr::Intersection { left: collection, right: value } => {
1260            symbol_appears_in_expr(sym, collection) || symbol_appears_in_expr(sym, value)
1261        }
1262        Expr::FieldAccess { object, .. } => symbol_appears_in_expr(sym, object),
1263        Expr::List(items) | Expr::Tuple(items) => {
1264            items.iter().any(|i| symbol_appears_in_expr(sym, i))
1265        }
1266        Expr::Range { start, end } => {
1267            symbol_appears_in_expr(sym, start) || symbol_appears_in_expr(sym, end)
1268        }
1269        Expr::New { init_fields, .. } => {
1270            init_fields.iter().any(|(_, v)| symbol_appears_in_expr(sym, v))
1271        }
1272        Expr::NewVariant { fields, .. } => {
1273            fields.iter().any(|(_, v)| symbol_appears_in_expr(sym, v))
1274        }
1275        Expr::OptionSome { value } => symbol_appears_in_expr(sym, value),
1276        Expr::WithCapacity { value, capacity } => {
1277            symbol_appears_in_expr(sym, value) || symbol_appears_in_expr(sym, capacity)
1278        }
1279        Expr::CallExpr { callee, args } => {
1280            symbol_appears_in_expr(sym, callee)
1281                || args.iter().any(|a| symbol_appears_in_expr(sym, a))
1282        }
1283        Expr::InterpolatedString(parts) => {
1284            parts.iter().any(|p| {
1285                if let crate::ast::stmt::StringPart::Expr { value, .. } = p {
1286                    symbol_appears_in_expr(sym, value)
1287                } else { false }
1288            })
1289        }
1290        Expr::Closure { body, .. } => {
1291            match body {
1292                crate::ast::stmt::ClosureBody::Expression(e) => symbol_appears_in_expr(sym, e),
1293                crate::ast::stmt::ClosureBody::Block(stmts) => stmts.iter().any(|s| symbol_appears_in_stmt(sym, s)),
1294            }
1295        }
1296        _ => false,
1297    }
1298}
1299
1300/// Check if a TypeExpr is a Vec/Seq/List type (collection that could be borrowed as &[T]).
1301pub(crate) fn is_vec_type_expr(ty: &TypeExpr, interner: &Interner) -> bool {
1302    match ty {
1303        TypeExpr::Generic { base, .. } => {
1304            let name = interner.resolve(*base);
1305            matches!(name, "Seq" | "List" | "Vec")
1306        }
1307        _ => false,
1308    }
1309}
1310
1311/// For a function's parameters and body, return the set of parameter indices
1312/// whose Vec<T> params are read-only (never mutated in the body).
1313/// These can safely be borrowed as &[T] instead of owned Vec<T>.
1314pub(super) fn collect_readonly_vec_param_indices(
1315    params: &[(Symbol, &TypeExpr)],
1316    body: &[Stmt],
1317    interner: &Interner,
1318) -> HashSet<usize> {
1319    let mutable_vars = collect_mutable_vars(body);
1320    let mut readonly_indices = HashSet::new();
1321    for (i, (param_sym, param_ty)) in params.iter().enumerate() {
1322        if is_vec_type_expr(param_ty, interner) && !mutable_vars.contains(param_sym) {
1323            readonly_indices.insert(i);
1324        }
1325    }
1326    readonly_indices
1327}
1328
1329/// Convert a Vec<T> or LogosSeq<T> type string to a slice &[T] type string.
1330/// E.g., "Vec<i64>" → "&[i64]", "LogosSeq<i64>" → "&[i64]"
1331pub(super) fn vec_to_slice_type(vec_type: &str) -> String {
1332    if let Some(inner) = vec_type.strip_prefix("Vec<").and_then(|s| s.strip_suffix('>')) {
1333        format!("&[{}]", inner)
1334    } else if let Some(inner) = vec_type.strip_prefix("LogosSeq<").and_then(|s| s.strip_suffix('>')) {
1335        format!("&[{}]", inner)
1336    } else {
1337        vec_type.to_string()
1338    }
1339}
1340
1341/// Convert `Vec<T>` or `LogosSeq<T>` to `&mut [T]` for mutable borrow parameters.
1342pub(super) fn vec_to_mut_slice_type(vec_type: &str) -> String {
1343    if let Some(inner) = vec_type.strip_prefix("Vec<").and_then(|s| s.strip_suffix('>')) {
1344        format!("&mut [{}]", inner)
1345    } else if let Some(inner) = vec_type.strip_prefix("LogosSeq<").and_then(|s| s.strip_suffix('>')) {
1346        format!("&mut [{}]", inner)
1347    } else {
1348        vec_type.to_string()
1349    }
1350}
1351
1352/// O2 de-Rc eligibility: Seq variables that provably never need reference
1353/// semantics, so codegen can emit a plain `Vec<T>` (no Rc/RefCell) instead of
1354/// `LogosSeq<T> = Rc<RefCell<Vec<T>>>`.
1355///
1356/// A variable qualifies when it is declared with a FRESH allocation of a Seq
1357/// whose element type is a scalar (Int/Float/Bool/Char/Text — reading an
1358/// element copies, never shares a handle) and, across the whole scope, it is
1359/// never aliased by a second live handle (`Let b be a`, `Set b to a`, stored
1360/// as an element) and never escapes (call arg, return, given away).
1361///
1362/// Conservative by construction: anything uncertain stays `LogosSeq`. An
1363/// unsound de-Rc would make the generated Rust fail to compile (two owners /
1364/// moved-from use), a loud failure the tests catch — never silent corruption.
1365pub(crate) fn collect_de_rc_seqs(
1366    stmts: &[Stmt],
1367    interner: &Interner,
1368    borrow_params: &HashMap<Symbol, HashSet<usize>>,
1369    mut_borrow_params: &HashMap<Symbol, HashSet<usize>>,
1370    vec_return_fns: &HashSet<Symbol>,
1371    returns_vec: bool,
1372) -> HashSet<Symbol> {
1373    // Kill switch (benchmark A/B): `LOGOS_DERC=0` keeps every Seq as `LogosSeq`.
1374    if !crate::optimize::active_config().is_on(crate::optimization::Opt::Unbox) {
1375        return HashSet::new();
1376    }
1377    // Phase 3a/3b: a candidate passed at EITHER a readonly-borrow (`&[T]`) or a
1378    // mutable-borrow (`&mut [T]`) param slot is only borrowed for the call, not
1379    // retained — that is not an escape. Both call conventions pass a reference
1380    // to the de-Rc'd `Vec`, so the use-scan treats both as safe slots. (Codegen
1381    // distinguishes `&x` vs `&mut x` via its own borrow maps.)
1382    let borrow_slots: HashMap<Symbol, HashSet<usize>> = if mut_borrow_params.is_empty() {
1383        borrow_params.clone()
1384    } else {
1385        let mut m = borrow_params.clone();
1386        for (f, idx) in mut_borrow_params {
1387            m.entry(*f).or_default().extend(idx.iter().copied());
1388        }
1389        m
1390    };
1391    let borrow_params = &borrow_slots;
1392    let mut candidates = HashSet::new();
1393    collect_de_rc_candidates_block(stmts, interner, vec_return_fns, &mut candidates);
1394    if candidates.is_empty() {
1395        return candidates;
1396    }
1397    // A candidate survives ONLY if every one of its occurrences is in a
1398    // Vec-safe slot (the collection of push/pop/add/remove/setindex/index/
1399    // length, or a fresh-Seq decl/reassign). Any other occurrence — call arg,
1400    // return, given/shown whole, sliced, copied, stored as an element,
1401    // aliased, or inside any statement/expression kind not explicitly
1402    // permitted — disqualifies it. Conservative by construction: the worst
1403    // outcome of the catch-alls is a missed optimization, never an unsound
1404    // `Vec` used where a `LogosSeq` is required.
1405    // Phase 2: buffer-reuse swap pairs. `Set outer to inner`, where `inner` is
1406    // a fresh-per-iteration buffer, is lowered by codegen to `std::mem::swap`
1407    // (a content exchange, NOT a shared handle), so it must not be treated as
1408    // aliasing. Collect those (outer, inner) pairs so the use-scan exempts them.
1409    let mut swaps = HashSet::new();
1410    collect_buffer_swap_pairs(stmts, interner, &mut swaps);
1411
1412    let mut disqualified = HashSet::new();
1413    derc_scan_uses(stmts, &candidates, &swaps, borrow_params, mut_borrow_params, vec_return_fns, returns_vec, interner, &mut disqualified);
1414    candidates.retain(|c| !disqualified.contains(c));
1415
1416    // A swap pair must de-Rc together — `std::mem::swap` needs both partners to
1417    // be the same type. If either is disqualified, drop both. Iterate to a
1418    // fixpoint so chained pairs settle.
1419    loop {
1420        let mut changed = false;
1421        for (outer, inner) in &swaps {
1422            if candidates.contains(outer) != candidates.contains(inner) {
1423                changed |= candidates.remove(outer);
1424                changed |= candidates.remove(inner);
1425            }
1426        }
1427        if !changed {
1428            break;
1429        }
1430    }
1431    if !candidates.is_empty() {
1432        crate::optimize::mark_fired(crate::optimization::Opt::Unbox);
1433    }
1434    candidates
1435}
1436
1437/// Phase 4 — return-type de-Rc. A function declared to return `Seq of T` can
1438/// instead return `Vec<T>` when EVERY `Return` yields a uniquely-owned value:
1439/// a freshly-built local Seq (moved out), a readonly-BORROW param (copied via
1440/// `.to_vec()` — a borrow can't alias), a fresh-Seq expression, or a call to
1441/// another such function. Returning `Vec` removes the per-call Rc clone +
1442/// RefCell borrow and the Rc box, and — crucially — makes `Set x to f(...)` at
1443/// every call site a uniquely-owned fresh value, unblocking de-Rc on `x` (the
1444/// mergesort `left`/`right`/`result` chain, currently disqualified because the
1445/// callee returns `LogosSeq`). Fixpoint over the callgraph so a function that
1446/// returns a call to a not-yet-confirmed peer settles.
1447pub(super) fn collect_vec_return_fns(
1448    stmts: &[Stmt],
1449    interner: &Interner,
1450    borrow_params: &HashMap<Symbol, HashSet<usize>>,
1451    mut_borrow_params: &HashMap<Symbol, HashSet<usize>>,
1452) -> HashSet<Symbol> {
1453    if !crate::optimize::active_config().is_on(crate::optimization::Opt::Unbox) {
1454        return HashSet::new();
1455    }
1456    // Every non-native function declared to return a Seq/List/Vec, with its
1457    // body + params. Each is a CANDIDATE to instead return an owned `Vec<T>`.
1458    let mut seq_fns: Vec<(Symbol, &[Stmt], &[(Symbol, &TypeExpr)])> = Vec::new();
1459    for s in stmts {
1460        if let Stmt::FunctionDef {
1461            name, body, params, return_type: Some(rt), is_native: false, ..
1462        } = s
1463        {
1464            if is_vec_type_expr(rt, interner) {
1465                seq_fns.push((*name, body, params));
1466            }
1467        }
1468    }
1469    if seq_fns.is_empty() {
1470        return HashSet::new();
1471    }
1472
1473    // GREATEST FIXPOINT — return-type de-Rc is an interprocedural aliasing
1474    // problem co-dependent with each scope's local de-Rc set: a function `f`
1475    // returns `Vec` only if every return site AND every call site is owned-Vec
1476    // compatible, while a call site's result de-Rc's only if `f` returns `Vec`.
1477    // Start OPTIMISTIC (every candidate returns `Vec`), then SHRINK on any
1478    // soundness violation until stable. Monotone (removals only) ⇒ converges.
1479    //
1480    // `f` is removed when ANY holds (each would emit ill-typed `Vec`/`LogosSeq`
1481    // code, never silent corruption — the violations are type errors):
1482    //   (a) a `Return` in `f` yields a value that is not an owned Vec — i.e. NOT
1483    //       a readonly-borrow param (returns `.to_vec()`) and NOT a local that
1484    //       itself de-Rc's (returns the moved `Vec`). A LogosSeq local return
1485    //       would be `return os;` into a `-> Vec` signature.
1486    //   (b) a call site `Let/Set x to f(...)` whose result `x` does not de-Rc —
1487    //       a `Vec` assigned into a `LogosSeq` variable.
1488    //   (c) `f(...)` used in ANY inline / non-binding position (struct field,
1489    //       owned arg, `Return f(x)`, `Show f(x)`, nested expression) — a `Vec`
1490    //       where a `LogosSeq` is expected. Conservative: only a top-level
1491    //       `Let/Set x to f(...)` binding consumes the owned `Vec` cleanly.
1492    let mut vec_fns: HashSet<Symbol> = seq_fns.iter().map(|(n, _, _)| *n).collect();
1493    loop {
1494        let mut remove = HashSet::new();
1495
1496        // (c) any inline use anywhere in the program (recurses into fn bodies).
1497        flag_inline_vec_calls(stmts, &vec_fns, &mut remove);
1498
1499        // (a)+(b), per scope, with that scope's de-Rc set computed under the
1500        // CURRENT candidate set. Main's scope is the whole program slice
1501        // (`collect_de_rc_seqs` skips nested fn defs); each function's scope is
1502        // its own body with `returns_vec` = whether it is still a candidate.
1503        let main_de_rc = collect_de_rc_seqs(stmts, interner, borrow_params, mut_borrow_params, &vec_fns, false);
1504        collect_unsound_vec_returns(stmts, &vec_fns, &main_de_rc, &mut remove);
1505        for (name, body, params) in &seq_fns {
1506            let rv = vec_fns.contains(name);
1507            let de_rc = collect_de_rc_seqs(body, interner, borrow_params, mut_borrow_params, &vec_fns, rv);
1508            collect_unsound_vec_returns(body, &vec_fns, &de_rc, &mut remove);
1509            if rv && !all_returns_vec_ownable(body, params, borrow_params.get(name), &de_rc) {
1510                remove.insert(*name);
1511            }
1512        }
1513
1514        if remove.is_empty() {
1515            break;
1516        }
1517        vec_fns.retain(|f| !remove.contains(f));
1518    }
1519    if !vec_fns.is_empty() {
1520        crate::optimize::mark_fired(crate::optimization::Opt::Unbox);
1521    }
1522    vec_fns
1523}
1524
1525/// Record any `f ∈ vec_fns` that has a call site `Let/Set x to f(...)` whose
1526/// result variable `x` is NOT de-Rc'd in this scope — returning `Vec` there
1527/// would assign a `Vec` into a `LogosSeq` variable.
1528fn collect_unsound_vec_returns(
1529    stmts: &[Stmt],
1530    vec_fns: &HashSet<Symbol>,
1531    de_rc: &HashSet<Symbol>,
1532    remove: &mut HashSet<Symbol>,
1533) {
1534    for stmt in stmts {
1535        let binding = match stmt {
1536            Stmt::Let { var, value, .. } => Some((*var, value)),
1537            Stmt::Set { target, value } => Some((*target, value)),
1538            _ => None,
1539        };
1540        if let Some((x, Expr::Call { function, .. })) = binding {
1541            if vec_fns.contains(function) && !de_rc.contains(&x) {
1542                remove.insert(*function);
1543            }
1544        }
1545        match stmt {
1546            Stmt::If { then_block, else_block, .. } => {
1547                collect_unsound_vec_returns(then_block, vec_fns, de_rc, remove);
1548                if let Some(eb) = else_block {
1549                    collect_unsound_vec_returns(eb, vec_fns, de_rc, remove);
1550                }
1551            }
1552            Stmt::While { body, .. } | Stmt::Repeat { body, .. } | Stmt::Zone { body, .. } => {
1553                collect_unsound_vec_returns(body, vec_fns, de_rc, remove)
1554            }
1555            _ => {}
1556        }
1557    }
1558}
1559
1560/// Condition (a): every `Return` in `body` yields a value the codegen can hand
1561/// back as an owned `Vec<T>`. Only two return shapes are owned-Vec compatible:
1562///   - a readonly-BORROW param (idx ∈ `borrow_idx`) → `return p.to_vec();` (a
1563///     copy — a borrow can never alias the caller's value), or
1564///   - a LOCAL that itself de-Rc's (`x ∈ de_rc`) → `return x;` (the moved Vec).
1565/// Anything else — a LogosSeq local, a bare fresh/slice/copy expression, an
1566/// owned param, a valueless `Return` — would emit a value of the wrong type
1567/// into the `-> Vec<T>` signature, so it disqualifies the function. (Inline
1568/// peer-call returns like `Return g(x)` are handled by the inline-use check.)
1569fn all_returns_vec_ownable(
1570    body: &[Stmt],
1571    params: &[(Symbol, &TypeExpr)],
1572    borrow_idx: Option<&HashSet<usize>>,
1573    de_rc: &HashSet<Symbol>,
1574) -> bool {
1575    fn walk(
1576        stmts: &[Stmt],
1577        params: &[(Symbol, &TypeExpr)],
1578        borrow_idx: Option<&HashSet<usize>>,
1579        de_rc: &HashSet<Symbol>,
1580        ok: &mut bool,
1581    ) {
1582        for s in stmts {
1583            match s {
1584                Stmt::Return { value: Some(e) } => {
1585                    if !is_vec_ownable_return(e, params, borrow_idx, de_rc) {
1586                        *ok = false;
1587                    }
1588                }
1589                Stmt::Return { value: None } => *ok = false,
1590                Stmt::If { then_block, else_block, .. } => {
1591                    walk(then_block, params, borrow_idx, de_rc, ok);
1592                    if let Some(eb) = else_block {
1593                        walk(eb, params, borrow_idx, de_rc, ok);
1594                    }
1595                }
1596                Stmt::While { body, .. } | Stmt::Repeat { body, .. } | Stmt::Zone { body, .. } => {
1597                    walk(body, params, borrow_idx, de_rc, ok)
1598                }
1599                _ => {}
1600            }
1601        }
1602    }
1603    let mut ok = true;
1604    walk(body, params, borrow_idx, de_rc, &mut ok);
1605    ok
1606}
1607
1608fn is_vec_ownable_return(
1609    e: &Expr,
1610    params: &[(Symbol, &TypeExpr)],
1611    borrow_idx: Option<&HashSet<usize>>,
1612    de_rc: &HashSet<Symbol>,
1613) -> bool {
1614    match e {
1615        Expr::Identifier(x) => match params.iter().position(|(p, _)| p == x) {
1616            // A readonly-BORROW param: `return p.to_vec();` — a copy, cannot alias.
1617            Some(idx) => borrow_idx.map_or(false, |b| b.contains(&idx)),
1618            // A LOCAL that de-Rc's: it is already a `Vec<T>` → `return x;`.
1619            None => de_rc.contains(x),
1620        },
1621        _ => false,
1622    }
1623}
1624
1625/// Condition (c): flag every `f ∈ vec_fns` whose call result is consumed in an
1626/// inline / non-binding position — anywhere other than the outermost RHS of a
1627/// top-level `Let/Set x to f(...)`. Such a position (struct field, owned arg,
1628/// `Return f(x)`, `Show f(x)`, a nested sub-expression, a control condition)
1629/// expects a `LogosSeq`, so an owned `Vec` return would not type-check.
1630/// Recurses into nested function bodies. Uses `symbol_appears_in_*` (the same
1631/// complete walker the de-Rc disqualification rests on) so its coverage matches.
1632fn flag_inline_vec_calls(
1633    stmts: &[Stmt],
1634    vec_fns: &HashSet<Symbol>,
1635    remove: &mut HashSet<Symbol>,
1636) {
1637    let flag_in_expr = |e: &Expr, remove: &mut HashSet<Symbol>| {
1638        for &f in vec_fns {
1639            if symbol_appears_in_expr(f, e) {
1640                remove.insert(f);
1641            }
1642        }
1643    };
1644    for stmt in stmts {
1645        match stmt {
1646            Stmt::Let { value, .. } | Stmt::Set { value, .. } => {
1647                // The outermost call of a binding RHS is the one SAFE consuming
1648                // position (its result is named; `collect_unsound_vec_returns`
1649                // separately requires that name to de-Rc). Only the call's ARGS
1650                // are inline positions here.
1651                if let Expr::Call { args, .. } = value {
1652                    for a in args.iter() {
1653                        flag_in_expr(a, remove);
1654                    }
1655                } else {
1656                    flag_in_expr(value, remove);
1657                }
1658            }
1659            Stmt::If { cond, then_block, else_block } => {
1660                flag_in_expr(cond, remove);
1661                flag_inline_vec_calls(then_block, vec_fns, remove);
1662                if let Some(eb) = else_block {
1663                    flag_inline_vec_calls(eb, vec_fns, remove);
1664                }
1665            }
1666            Stmt::While { cond, body, .. } => {
1667                flag_in_expr(cond, remove);
1668                flag_inline_vec_calls(body, vec_fns, remove);
1669            }
1670            Stmt::Repeat { iterable, body, .. } => {
1671                flag_in_expr(iterable, remove);
1672                flag_inline_vec_calls(body, vec_fns, remove);
1673            }
1674            Stmt::Zone { body, .. } | Stmt::FunctionDef { body, .. } => {
1675                flag_inline_vec_calls(body, vec_fns, remove)
1676            }
1677            // Every other statement kind (Return, Show, Give, Push, Add, Remove,
1678            // SetIndex, Call, Inspect, …): a vec-fn call anywhere inside is an
1679            // inline use. `symbol_appears_in_stmt` is the complete walker.
1680            other => {
1681                for &f in vec_fns {
1682                    if symbol_appears_in_stmt(f, other) {
1683                        remove.insert(f);
1684                    }
1685                }
1686            }
1687        }
1688    }
1689}
1690
1691/// `value` is a call to a function that returns an owned `Vec` (Phase 4) — so
1692/// `Set x to value` binds `x` to a uniquely-owned fresh value, not a shared
1693/// callee handle.
1694fn is_vec_return_call(value: &Expr, vec_return_fns: &HashSet<Symbol>) -> bool {
1695    matches!(value, Expr::Call { function, .. } if vec_return_fns.contains(function))
1696}
1697
1698/// Phase 3b: `value` is a call `f(..., target, ...)` where `f` mutably borrows
1699/// `target` at the position `target` appears. Codegen lowers `Set target to
1700/// f(...)` to the in-place call `f(&mut target, ...)`, so it is a mutation of
1701/// `target`, not a rebinding — `target` keeps its identity and de-Rc eligibility.
1702fn is_mut_borrow_inplace_call(
1703    target: Symbol,
1704    value: &Expr,
1705    mut_borrow_params: &HashMap<Symbol, HashSet<usize>>,
1706) -> bool {
1707    if let Expr::Call { function, args } = value {
1708        if let Some(slots) = mut_borrow_params.get(function) {
1709            return args.iter().enumerate().any(|(i, a)| {
1710                slots.contains(&i) && matches!(a, Expr::Identifier(s) if *s == target)
1711            });
1712        }
1713    }
1714    false
1715}
1716
1717/// Collect `(outer, inner)` pairs where `Set outer to inner` is a buffer-reuse
1718/// swap: `inner` is declared `Let mutable inner = <fresh Seq>` earlier in the
1719/// same loop body and is not referenced after the `Set`. These exactly mirror
1720/// `detect_buffer_reuse_in_body`'s shape, so codegen lowers each to
1721/// `std::mem::swap` — content exchange, not aliasing.
1722fn collect_buffer_swap_pairs(
1723    stmts: &[Stmt],
1724    interner: &Interner,
1725    out: &mut HashSet<(Symbol, Symbol)>,
1726) {
1727    for stmt in stmts {
1728        match stmt {
1729            Stmt::While { body, .. } | Stmt::Repeat { body, .. } => {
1730                detect_swap_in_body(body, interner, out);
1731                collect_buffer_swap_pairs(body, interner, out);
1732            }
1733            Stmt::If { then_block, else_block, .. } => {
1734                collect_buffer_swap_pairs(then_block, interner, out);
1735                if let Some(eb) = else_block {
1736                    collect_buffer_swap_pairs(eb, interner, out);
1737                }
1738            }
1739            Stmt::Zone { body, .. } => collect_buffer_swap_pairs(body, interner, out),
1740            _ => {}
1741        }
1742    }
1743}
1744
1745fn detect_swap_in_body(body: &[Stmt], interner: &Interner, out: &mut HashSet<(Symbol, Symbol)>) {
1746    let mut fresh_inner: HashSet<Symbol> = HashSet::new();
1747    for stmt in body {
1748        if let Stmt::Let { var, value, mutable: true, .. } = stmt {
1749            if is_fresh_seq_value(value, interner) {
1750                fresh_inner.insert(*var);
1751            }
1752        }
1753    }
1754    if fresh_inner.is_empty() {
1755        return;
1756    }
1757    for (idx, stmt) in body.iter().enumerate() {
1758        if let Stmt::Set { target, value: Expr::Identifier(src) } = stmt {
1759            if *target != *src && fresh_inner.contains(src) {
1760                let used_after = body[idx + 1..]
1761                    .iter()
1762                    .any(|s| symbol_appears_in_stmt(*src, s));
1763                if !used_after {
1764                    out.insert((*target, *src));
1765                }
1766            }
1767        }
1768    }
1769}
1770
1771/// Walk every statement, disqualifying any candidate that appears outside a
1772/// Vec-safe slot. Buffer-reuse swap pairs are exempt from the alias rule.
1773fn derc_scan_uses(
1774    stmts: &[Stmt],
1775    cands: &HashSet<Symbol>,
1776    swaps: &HashSet<(Symbol, Symbol)>,
1777    borrow_params: &HashMap<Symbol, HashSet<usize>>,
1778    mut_borrow_params: &HashMap<Symbol, HashSet<usize>>,
1779    vec_return_fns: &HashSet<Symbol>,
1780    returns_vec: bool,
1781    interner: &Interner,
1782    dq: &mut HashSet<Symbol>,
1783) {
1784    for stmt in stmts {
1785        derc_scan_stmt(stmt, cands, swaps, borrow_params, mut_borrow_params, vec_return_fns, returns_vec, interner, dq);
1786    }
1787}
1788
1789/// Phase 3: a candidate passed at a READONLY-borrow-param position (`&[T]`) is
1790/// only borrowed, never retained — that is NOT an escape, so it stays eligible
1791/// and the call site passes `&vec`. A candidate at any other arg position
1792/// (owned param, or a callee with no borrow info) escapes and is disqualified.
1793fn derc_scan_call_args(
1794    function: Symbol,
1795    args: &[&Expr],
1796    cands: &HashSet<Symbol>,
1797    borrow_params: &HashMap<Symbol, HashSet<usize>>,
1798    dq: &mut HashSet<Symbol>,
1799) {
1800    let slots = borrow_params.get(&function);
1801    for (i, a) in args.iter().enumerate() {
1802        match a {
1803            Expr::Identifier(s) if cands.contains(s) => {
1804                let is_borrow_slot = slots.map_or(false, |set| set.contains(&i));
1805                if !is_borrow_slot {
1806                    dq.insert(*s);
1807                }
1808            }
1809            _ => derc_scan_expr(a, cands, borrow_params, dq),
1810        }
1811    }
1812}
1813
1814/// True if `value` is a fresh Seq/List allocation (a safe decl/reassign source).
1815fn is_fresh_seq_value(value: &Expr, interner: &Interner) -> bool {
1816    match value {
1817        Expr::New { type_name, init_fields, .. } if init_fields.is_empty() => {
1818            matches!(interner.resolve(*type_name), "Seq" | "List" | "Vec")
1819        }
1820        Expr::WithCapacity { value: inner, .. } => is_fresh_seq_value(inner, interner),
1821        Expr::List(_) => true,
1822        _ => false,
1823    }
1824}
1825
1826fn derc_scan_stmt(
1827    stmt: &Stmt,
1828    cands: &HashSet<Symbol>,
1829    swaps: &HashSet<(Symbol, Symbol)>,
1830    borrow_params: &HashMap<Symbol, HashSet<usize>>,
1831    mut_borrow_params: &HashMap<Symbol, HashSet<usize>>,
1832    vec_return_fns: &HashSet<Symbol>,
1833    returns_vec: bool,
1834    interner: &Interner,
1835    dq: &mut HashSet<Symbol>,
1836) {
1837    match stmt {
1838        // A fresh-Seq decl is the safe origin; otherwise the initializer is a
1839        // general use of whatever it references.
1840        Stmt::Let { value, .. } => {
1841            if is_vec_return_call(value, vec_return_fns) {
1842                // Phase 4: `Let r be f(...)` with f returning an OWNED Vec — r
1843                // captures a uniquely-owned fresh value, not a shared handle.
1844                // Still scan the call's args.
1845                if let Expr::Call { function, args } = value {
1846                    derc_scan_call_args(*function, args, cands, borrow_params, dq);
1847                }
1848            } else if !is_fresh_seq_value(value, interner) {
1849                derc_scan_expr(value, cands, borrow_params, dq);
1850            }
1851        }
1852        Stmt::Set { target, value } => {
1853            if let Expr::Identifier(src) = value {
1854                if swaps.contains(&(*target, *src)) {
1855                    // Buffer-reuse swap → content exchange (`std::mem::swap`),
1856                    // not aliasing. Both partners stay eligible.
1857                } else if target != src {
1858                    // `Set x to y` rebinds x onto y's allocation — both lose
1859                    // unique ownership.
1860                    dq.insert(*src);
1861                    dq.insert(*target);
1862                }
1863            } else if is_vec_return_call(value, vec_return_fns) {
1864                // Phase 4: `Set x to f(...)` where f returns an OWNED Vec — x
1865                // captures a uniquely-owned fresh value, not a shared callee
1866                // handle, so x stays eligible. Still scan the call's args (a
1867                // candidate passed to a NON-borrow param would escape).
1868                if let Expr::Call { function, args } = value {
1869                    derc_scan_call_args(*function, args, cands, borrow_params, dq);
1870                }
1871            } else if is_mut_borrow_inplace_call(*target, value, mut_borrow_params) {
1872                // Phase 3b: `Set x to f(..., x, ...)` where `f` mut-borrows `x`
1873                // at x's position is lowered to an IN-PLACE call `f(&mut x, ...)`
1874                // — the "result" is x itself, not a reassignment, so x keeps its
1875                // unique ownership and stays eligible. Scan the OTHER args (a
1876                // candidate passed to a non-borrow slot still escapes).
1877                if let Expr::Call { function, args } = value {
1878                    derc_scan_call_args(*function, args, cands, borrow_params, dq);
1879                }
1880            } else if !is_fresh_seq_value(value, interner) {
1881                // `Set x to <non-fresh value>` (a call result, slice, copy, …)
1882                // rebinds x to a value that is NOT a uniquely-owned fresh Vec —
1883                // e.g. `Set right to mergeSort(right)` binds right to a callee's
1884                // returned `LogosSeq`. Disqualify the target.
1885                dq.insert(*target);
1886                derc_scan_expr(value, cands, borrow_params, dq);
1887            }
1888        }
1889        // Collection slots are safe for a bare candidate; the value/index are
1890        // general uses.
1891        Stmt::Push { value, collection } | Stmt::Add { value, collection } => {
1892            derc_scan_collection_slot(collection, cands, borrow_params, dq);
1893            derc_scan_expr(value, cands, borrow_params, dq);
1894        }
1895        Stmt::Pop { collection, .. } => derc_scan_collection_slot(collection, cands, borrow_params, dq),
1896        Stmt::Remove { value, collection } => {
1897            derc_scan_collection_slot(collection, cands, borrow_params, dq);
1898            derc_scan_expr(value, cands, borrow_params, dq);
1899        }
1900        Stmt::SetIndex { collection, index, value } => {
1901            derc_scan_collection_slot(collection, cands, borrow_params, dq);
1902            derc_scan_expr(index, cands, borrow_params, dq);
1903            derc_scan_expr(value, cands, borrow_params, dq);
1904        }
1905        Stmt::If { cond, then_block, else_block } => {
1906            derc_scan_expr(cond, cands, borrow_params, dq);
1907            derc_scan_uses(then_block, cands, swaps, borrow_params, mut_borrow_params, vec_return_fns, returns_vec, interner, dq);
1908            if let Some(eb) = else_block {
1909                derc_scan_uses(eb, cands, swaps, borrow_params, mut_borrow_params, vec_return_fns, returns_vec, interner, dq);
1910            }
1911        }
1912        Stmt::While { cond, body, .. } => {
1913            derc_scan_expr(cond, cands, borrow_params, dq);
1914            derc_scan_uses(body, cands, swaps, borrow_params, mut_borrow_params, vec_return_fns, returns_vec, interner, dq);
1915        }
1916        Stmt::Repeat { iterable, body, .. } => {
1917            // Iterating a de-Rc'd Vec is not lowered in v1 → treat as a use.
1918            derc_scan_expr(iterable, cands, borrow_params, dq);
1919            derc_scan_uses(body, cands, swaps, borrow_params, mut_borrow_params, vec_return_fns, returns_vec, interner, dq);
1920        }
1921        Stmt::Zone { body, .. } => derc_scan_uses(body, cands, swaps, borrow_params, mut_borrow_params, vec_return_fns, returns_vec, interner, dq),
1922        // Statements that carry expressions which may legitimately pass a
1923        // candidate to a borrow-param: scan position-aware.
1924        Stmt::Call { function, args } => {
1925            derc_scan_call_args(*function, args, cands, borrow_params, dq);
1926        }
1927        // Phase 4: in a Vec-returning function, returning a candidate LOCAL
1928        // moves it out as the owned Vec result — not a disqualifying escape.
1929        Stmt::Return { value: Some(e) } => {
1930            if !(returns_vec && matches!(e, Expr::Identifier(x) if cands.contains(x))) {
1931                derc_scan_expr(e, cands, borrow_params, dq);
1932            }
1933        }
1934        Stmt::Return { value: None } => {}
1935        Stmt::Show { object, recipient } | Stmt::Give { object, recipient } => {
1936            derc_scan_expr(object, cands, borrow_params, dq);
1937            derc_scan_expr(recipient, cands, borrow_params, dq);
1938        }
1939        // A nested function is a SEPARATE scope with its own de-Rc analysis. Its
1940        // parameter/local symbols are distinct bindings even when they reuse an
1941        // outer name (the interner gives `arr` one Symbol, but main's `arr` and a
1942        // function's `arr` param are different variables). The outer scan must
1943        // NOT descend — a candidate can only reach a function through explicit
1944        // call args, which are checked at the call site. Skipping keeps a
1945        // name-collision from spuriously disqualifying an outer candidate.
1946        Stmt::FunctionDef { .. } => {}
1947        // Every other statement kind (SetField, Concurrent, …): any candidate
1948        // that appears anywhere in it is an unsafe use. `symbol_appears_in_stmt`
1949        // is a complete walker.
1950        other => {
1951            for &c in cands {
1952                if symbol_appears_in_stmt(c, other) {
1953                    dq.insert(c);
1954                }
1955            }
1956        }
1957    }
1958}
1959
1960/// The collection position of an indexing/mutation op: a bare candidate
1961/// identifier here is the SAFE Vec target; a nested expression is scanned.
1962fn derc_scan_collection_slot(
1963    collection: &Expr,
1964    cands: &HashSet<Symbol>,
1965    borrow_params: &HashMap<Symbol, HashSet<usize>>,
1966    dq: &mut HashSet<Symbol>,
1967) {
1968    match collection {
1969        Expr::Identifier(_) => {}
1970        other => derc_scan_expr(other, cands, borrow_params, dq),
1971    }
1972}
1973
1974/// Disqualify any candidate that appears in `expr` outside an index/length
1975/// collection slot. Expression kinds that may legitimately read a candidate by
1976/// index (BinaryOp, Not, Call args) are recursed position-aware; every other
1977/// kind is treated conservatively — any candidate inside is an unsafe use.
1978fn derc_scan_expr(
1979    expr: &Expr,
1980    cands: &HashSet<Symbol>,
1981    borrow_params: &HashMap<Symbol, HashSet<usize>>,
1982    dq: &mut HashSet<Symbol>,
1983) {
1984    match expr {
1985        Expr::Identifier(s) => {
1986            if cands.contains(s) {
1987                dq.insert(*s);
1988            }
1989        }
1990        Expr::Index { collection, index } => {
1991            derc_scan_collection_slot(collection, cands, borrow_params, dq);
1992            derc_scan_expr(index, cands, borrow_params, dq);
1993        }
1994        Expr::Length { collection } => derc_scan_collection_slot(collection, cands, borrow_params, dq),
1995        Expr::BinaryOp { left, right, .. } => {
1996            derc_scan_expr(left, cands, borrow_params, dq);
1997            derc_scan_expr(right, cands, borrow_params, dq);
1998        }
1999        Expr::Not { operand } => derc_scan_expr(operand, cands, borrow_params, dq),
2000        Expr::Call { function, args } => {
2001            derc_scan_call_args(*function, args, cands, borrow_params, dq);
2002        }
2003        // Slice, Copy, FieldAccess, interpolation, New init-fields, and any
2004        // other kind: a candidate appearing inside is an unsafe use.
2005        // `symbol_appears_in_expr` is a complete walker.
2006        other => {
2007            for &c in cands {
2008                if symbol_appears_in_expr(c, other) {
2009                    dq.insert(c);
2010                }
2011            }
2012        }
2013    }
2014}
2015
2016/// Rust element type of a homogeneous **scalar-literal** list, or `None` when the list is
2017/// empty, mixed-kind, or has any non-scalar-literal element. `[1,2,3]`→`i64`, `[1.0,…]`→`f64`,
2018/// `[true,…]`→`bool`, `['a',…]`→`char`. Such a list is a uniquely-owned fresh value with no
2019/// borrowed handle inside it, so it de-Rc's from `LogosSeq<T>` to a plain `Vec<T>`. Detection
2020/// (`fresh_scalar_seq_elem`) and codegen (`derc_vec_decl`) BOTH route through this one helper
2021/// so their eligibility can never drift.
2022pub(crate) fn homogeneous_scalar_literal_elem(items: &[&Expr]) -> Option<String> {
2023    fn elem_ty(e: &Expr) -> Option<&'static str> {
2024        match e {
2025            Expr::Literal(Literal::Number(_)) => Some("i64"),
2026            Expr::Literal(Literal::Float(_)) => Some("f64"),
2027            Expr::Literal(Literal::Boolean(_)) => Some("bool"),
2028            Expr::Literal(Literal::Char(_)) => Some("char"),
2029            _ => None,
2030        }
2031    }
2032    let first = elem_ty(items.first()?)?;
2033    if items.iter().all(|i| elem_ty(i) == Some(first)) {
2034        Some(first.to_string())
2035    } else {
2036        None
2037    }
2038}
2039
2040/// The Rust element type string if `value` freshly allocates a Seq of scalars.
2041fn fresh_scalar_seq_elem(value: &Expr, interner: &Interner) -> Option<String> {
2042    match value {
2043        Expr::New { type_name, type_args, init_fields } if init_fields.is_empty() => {
2044            match interner.resolve(*type_name) {
2045                "Seq" | "List" | "Vec" => {
2046                    let elem = type_args.first()?;
2047                    let ty = codegen_type_expr(elem, interner);
2048                    if is_scalar_elem_type(&ty) { Some(ty) } else { None }
2049                }
2050                _ => None,
2051            }
2052        }
2053        Expr::WithCapacity { value: inner, .. } => fresh_scalar_seq_elem(inner, interner),
2054        // A homogeneous SCALAR-literal list (`[1,2,3]`, `[1.0,…]`, `[true,…]`, `['a',…]`) is a
2055        // uniquely-owned fresh Seq — de-Rc it to a plain `Vec<T>` so indexed access skips the
2056        // RefCell borrow + the Rc box. The use-scan still disqualifies any occurrence that
2057        // escapes a Vec-safe slot.
2058        Expr::List(items) => homogeneous_scalar_literal_elem(items),
2059        _ => None,
2060    }
2061}
2062
2063/// Scalar element types whose reads copy (no shared handle): safe to de-Rc.
2064fn is_scalar_elem_type(ty: &str) -> bool {
2065    matches!(
2066        ty,
2067        "i64" | "i32" | "i16" | "i8" | "u64" | "u32" | "u16" | "u8" | "usize" | "isize"
2068            | "f64" | "f32" | "bool" | "char" | "String"
2069            // Fixed-width word newtypes are Copy scalars (repr(transparent)) → de-Rc to `Vec<WordN>`.
2070            | "Word8" | "Word16" | "Word32" | "Word64"
2071    )
2072}
2073
2074fn collect_de_rc_candidates_block(
2075    stmts: &[Stmt],
2076    interner: &Interner,
2077    vec_return_fns: &HashSet<Symbol>,
2078    out: &mut HashSet<Symbol>,
2079) {
2080    for stmt in stmts {
2081        match stmt {
2082            Stmt::Let { var, value, .. } => {
2083                // A fresh-Seq decl, or a binding to a Phase-4 Vec-returning call
2084                // (`Let r be buildSeq(4)`) — both capture a uniquely-owned value.
2085                if fresh_scalar_seq_elem(value, interner).is_some()
2086                    || is_vec_return_call(value, vec_return_fns)
2087                {
2088                    out.insert(*var);
2089                }
2090            }
2091            Stmt::If { then_block, else_block, .. } => {
2092                collect_de_rc_candidates_block(then_block, interner, vec_return_fns, out);
2093                if let Some(eb) = else_block {
2094                    collect_de_rc_candidates_block(eb, interner, vec_return_fns, out);
2095                }
2096            }
2097            Stmt::While { body, .. } | Stmt::Repeat { body, .. } => {
2098                collect_de_rc_candidates_block(body, interner, vec_return_fns, out);
2099            }
2100            Stmt::Zone { body, .. } => {
2101                collect_de_rc_candidates_block(body, interner, vec_return_fns, out);
2102            }
2103            _ => {}
2104        }
2105    }
2106}
2107
2108/// Collect locally-created Seq/List variables that escape the function body.
2109/// A variable "escapes" if it is:
2110/// - Passed to a function call (without `copy of` wrapper)
2111/// - Returned from the function
2112/// Variables NOT in the returned set can safely use `Vec<T>` instead of `LogosSeq<T>`.
2113pub(super) fn collect_escaping_collection_vars(stmts: &[Stmt], interner: &Interner) -> HashSet<Symbol> {
2114    let mut escaped = HashSet::new();
2115    collect_escaping_vars_block(stmts, &mut escaped);
2116    escaped
2117}
2118
2119fn collect_escaping_vars_block(stmts: &[Stmt], escaped: &mut HashSet<Symbol>) {
2120    for stmt in stmts {
2121        collect_escaping_vars_stmt(stmt, escaped);
2122    }
2123}
2124
2125fn collect_escaping_vars_stmt(stmt: &Stmt, escaped: &mut HashSet<Symbol>) {
2126    match stmt {
2127        Stmt::Call { args, .. } => {
2128            for arg in args.iter() {
2129                collect_escaping_vars_from_call_arg(arg, escaped);
2130            }
2131        }
2132        Stmt::Return { value: Some(expr) } => {
2133            if let Expr::Identifier(sym) = expr {
2134                escaped.insert(*sym);
2135            }
2136        }
2137        Stmt::If { then_block, else_block, .. } => {
2138            collect_escaping_vars_block(then_block, escaped);
2139            if let Some(else_stmts) = else_block {
2140                collect_escaping_vars_block(else_stmts, escaped);
2141            }
2142        }
2143        Stmt::While { body, .. } | Stmt::Repeat { body, .. } => {
2144            collect_escaping_vars_block(body, escaped);
2145        }
2146        Stmt::Let { value, .. } => {
2147            collect_escaping_vars_from_expr_calls(value, escaped);
2148        }
2149        Stmt::Set { value, .. } => {
2150            collect_escaping_vars_from_expr_calls(value, escaped);
2151        }
2152        _ => {}
2153    }
2154}
2155
2156/// Check if an expression is a direct call arg (not wrapped in Copy).
2157fn collect_escaping_vars_from_call_arg(expr: &Expr, escaped: &mut HashSet<Symbol>) {
2158    match expr {
2159        Expr::Identifier(sym) => { escaped.insert(*sym); }
2160        Expr::Copy { .. } => { /* copy of X does not cause X to escape */ }
2161        _ => {
2162            // For complex expressions, check nested identifiers
2163            collect_escaping_vars_from_expr_ids(expr, escaped);
2164        }
2165    }
2166}
2167
2168/// Collect identifiers that appear as direct args in Call expressions within an expression.
2169fn collect_escaping_vars_from_expr_calls(expr: &Expr, escaped: &mut HashSet<Symbol>) {
2170    match expr {
2171        Expr::Call { args, .. } => {
2172            for arg in args.iter() {
2173                collect_escaping_vars_from_call_arg(arg, escaped);
2174            }
2175        }
2176        Expr::BinaryOp { left, right, .. } => {
2177            collect_escaping_vars_from_expr_calls(left, escaped);
2178            collect_escaping_vars_from_expr_calls(right, escaped);
2179        }
2180        _ => {}
2181    }
2182}
2183
2184/// Collect direct identifier references from a non-Copy expression used as a call arg.
2185fn collect_escaping_vars_from_expr_ids(expr: &Expr, escaped: &mut HashSet<Symbol>) {
2186    match expr {
2187        Expr::Identifier(sym) => { escaped.insert(*sym); }
2188        Expr::BinaryOp { left, right, .. } => {
2189            collect_escaping_vars_from_expr_ids(left, escaped);
2190            collect_escaping_vars_from_expr_ids(right, escaped);
2191        }
2192        _ => {}
2193    }
2194}
2195
2196/// Extract a debug prefix string from an expression for `{var=}` format.
2197pub(super) fn expr_debug_prefix(expr: &Expr, interner: &Interner) -> String {
2198    match expr {
2199        Expr::Identifier(sym) => interner.resolve(*sym).to_string(),
2200        _ => "expr".to_string(),
2201    }
2202}
2203
2204/// Collect identifiers from a statement's expressions (for Concurrent/Parallel variable capture).
2205pub(super) fn collect_stmt_identifiers(stmt: &Stmt, identifiers: &mut HashSet<Symbol>) {
2206    match stmt {
2207        Stmt::Let { value, .. } => {
2208            collect_expr_identifiers(value, identifiers);
2209        }
2210        Stmt::Call { args, .. } => {
2211            for arg in args {
2212                collect_expr_identifiers(arg, identifiers);
2213            }
2214        }
2215        _ => {}
2216    }
2217}
2218
2219/// Collect parameter indices where any call site passes `Give` as the argument.
2220/// If a caller writes `Call f with Give x`, index 0 must not be borrowed as &[T].
2221pub(super) fn collect_give_arg_indices(fn_sym: Symbol, stmts: &[Stmt]) -> HashSet<usize> {
2222    let mut result = HashSet::new();
2223    for stmt in stmts {
2224        scan_give_args_stmt(fn_sym, stmt, &mut result);
2225    }
2226    result
2227}
2228
2229fn scan_give_args_stmt(fn_sym: Symbol, stmt: &Stmt, result: &mut HashSet<usize>) {
2230    match stmt {
2231        Stmt::Call { function, args } => {
2232            if *function == fn_sym {
2233                for (i, arg) in args.iter().enumerate() {
2234                    if matches!(arg, Expr::Give { .. }) {
2235                        result.insert(i);
2236                    }
2237                }
2238            }
2239            for arg in args {
2240                scan_give_args_expr(fn_sym, arg, result);
2241            }
2242        }
2243        Stmt::Let { value, .. } | Stmt::Set { value, .. } => {
2244            scan_give_args_expr(fn_sym, value, result);
2245        }
2246        Stmt::Return { value: Some(v) } => scan_give_args_expr(fn_sym, v, result),
2247        Stmt::FunctionDef { body, .. } => {
2248            for s in *body {
2249                scan_give_args_stmt(fn_sym, s, result);
2250            }
2251        }
2252        Stmt::If { then_block, else_block, .. } => {
2253            for s in *then_block {
2254                scan_give_args_stmt(fn_sym, s, result);
2255            }
2256            if let Some(b) = else_block {
2257                for s in *b {
2258                    scan_give_args_stmt(fn_sym, s, result);
2259                }
2260            }
2261        }
2262        Stmt::While { body, .. } | Stmt::Repeat { body, .. } | Stmt::Zone { body, .. } => {
2263            for s in *body {
2264                scan_give_args_stmt(fn_sym, s, result);
2265            }
2266        }
2267        Stmt::Concurrent { tasks } | Stmt::Parallel { tasks } => {
2268            for s in *tasks {
2269                scan_give_args_stmt(fn_sym, s, result);
2270            }
2271        }
2272        Stmt::Inspect { arms, .. } => {
2273            for arm in arms.iter() {
2274                for s in arm.body.iter() {
2275                    scan_give_args_stmt(fn_sym, s, result);
2276                }
2277            }
2278        }
2279        _ => {}
2280    }
2281}
2282
2283fn scan_give_args_expr(fn_sym: Symbol, expr: &Expr, result: &mut HashSet<usize>) {
2284    match expr {
2285        Expr::Call { function, args } => {
2286            if *function == fn_sym {
2287                for (i, arg) in args.iter().enumerate() {
2288                    if matches!(arg, Expr::Give { .. }) {
2289                        result.insert(i);
2290                    }
2291                }
2292            }
2293            for arg in args {
2294                scan_give_args_expr(fn_sym, arg, result);
2295            }
2296        }
2297        Expr::BinaryOp { left, right, .. } => {
2298            scan_give_args_expr(fn_sym, left, result);
2299            scan_give_args_expr(fn_sym, right, result);
2300        }
2301        Expr::FieldAccess { object, .. }
2302        | Expr::Give { value: object }
2303        | Expr::Copy { expr: object }
2304        | Expr::Length { collection: object } => {
2305            scan_give_args_expr(fn_sym, object, result);
2306        }
2307        Expr::Index { collection, index } => {
2308            scan_give_args_expr(fn_sym, collection, result);
2309            scan_give_args_expr(fn_sym, index, result);
2310        }
2311        Expr::List(items) | Expr::Tuple(items) => {
2312            for item in items {
2313                scan_give_args_expr(fn_sym, item, result);
2314            }
2315        }
2316        _ => {}
2317    }
2318}
2319
2320// =============================================================================
2321// Closed-Form Double Recursion Detection
2322// =============================================================================
2323
2324/// Result of detecting a closed-form double recursion pattern.
2325/// For f(0) = base, f(d) = k + f(d-1) + f(d-1), the closed form is:
2326/// f(d) = ((base + k) << d) - k
2327pub(super) struct ClosedFormInfo {
2328    pub base: i64,
2329    pub k: i64,
2330}
2331
2332/// Detect "double recursion with constant addend" pattern:
2333///   f(0) = base
2334///   f(d) = k + f(d-1) + f(d-1)
2335/// where base and k are integer constants. Returns the closed-form parameters.
2336///
2337/// GCC/LLVM recognize this pattern and replace it with bit shifts at -O2.
2338/// This detection lets Logos emit the same closed form.
2339pub(super) fn detect_double_recursion_closed_form<'a>(
2340    func_name: Symbol,
2341    params: &[(Symbol, &'a TypeExpr<'a>)],
2342    body: &'a [Stmt<'a>],
2343    interner: &Interner,
2344) -> Option<ClosedFormInfo> {
2345    use crate::ast::stmt::BinaryOpKind;
2346
2347    if params.len() != 1 {
2348        return None;
2349    }
2350    let param_sym = params[0].0;
2351
2352    // The closed form emits `((base + k) << d) - k`, which requires the parameter
2353    // `d` to be an integer (a `<<` on a float operand does not type-check). A bare
2354    // `0`/`1` base literal parses as an integer regardless of the declared
2355    // parameter type, so a Float-typed double recursion would otherwise still
2356    // match and emit `i64 << f64`. Decline anything but an integer parameter.
2357    let is_integer_param = matches!(
2358        params[0].1,
2359        TypeExpr::Primitive(sym) | TypeExpr::Named(sym)
2360            if matches!(interner.resolve(*sym), "Int" | "Nat" | "Byte")
2361    );
2362    if !is_integer_param {
2363        return None;
2364    }
2365
2366    // Body must be exactly: If { param == 0 → Return base }, Return(recursive_expr)
2367    if body.len() != 2 {
2368        return None;
2369    }
2370
2371    // First statement: base case check
2372    let base_value = match &body[0] {
2373        Stmt::If { cond, then_block, else_block } => {
2374            if else_block.is_some() {
2375                return None;
2376            }
2377            if then_block.len() != 1 {
2378                return None;
2379            }
2380            let base = match &then_block[0] {
2381                Stmt::Return { value: Some(expr) } => cf_extract_literal_int(expr)?,
2382                _ => return None,
2383            };
2384            match cond {
2385                Expr::BinaryOp { op: BinaryOpKind::Eq, left, right } => {
2386                    let ok = (cf_is_identifier(left, param_sym) && cf_is_literal_int(right, 0))
2387                        || (cf_is_literal_int(left, 0) && cf_is_identifier(right, param_sym));
2388                    if !ok { return None; }
2389                }
2390                _ => return None,
2391            }
2392            base
2393        }
2394        _ => return None,
2395    };
2396
2397    // Second statement: recursive case Return
2398    let recursive_expr = match &body[1] {
2399        Stmt::Return { value: Some(expr) } => *expr,
2400        _ => return None,
2401    };
2402
2403    // Flatten the Add tree and analyze: must have exactly 2 self-calls with
2404    // arg (param - 1), and any number of integer literal addends.
2405    let mut self_call_count = 0usize;
2406    let mut constant_sum = 0i64;
2407    if !cf_analyze_add_tree(recursive_expr, func_name, param_sym, &mut self_call_count, &mut constant_sum) {
2408        return None;
2409    }
2410
2411    if self_call_count != 2 {
2412        return None;
2413    }
2414
2415    Some(ClosedFormInfo { base: base_value, k: constant_sum })
2416}
2417
2418fn cf_extract_literal_int(expr: &Expr) -> Option<i64> {
2419    if let Expr::Literal(crate::ast::stmt::Literal::Number(n)) = expr {
2420        Some(*n)
2421    } else {
2422        None
2423    }
2424}
2425
2426fn cf_is_identifier(expr: &Expr, sym: Symbol) -> bool {
2427    matches!(expr, Expr::Identifier(s) if *s == sym)
2428}
2429
2430fn cf_is_literal_int(expr: &Expr, val: i64) -> bool {
2431    matches!(expr, Expr::Literal(crate::ast::stmt::Literal::Number(n)) if *n == val)
2432}
2433
2434fn cf_is_self_call_with_decrement(expr: &Expr, func_name: Symbol, param_sym: Symbol) -> bool {
2435    use crate::ast::stmt::BinaryOpKind;
2436    if let Expr::Call { function, args } = expr {
2437        if *function != func_name || args.len() != 1 {
2438            return false;
2439        }
2440        if let Expr::BinaryOp { op: BinaryOpKind::Subtract, left, right } = args[0] {
2441            cf_is_identifier(left, param_sym) && cf_is_literal_int(right, 1)
2442        } else {
2443            false
2444        }
2445    } else {
2446        false
2447    }
2448}
2449
2450/// Walk an Add-tree, counting self-calls and summing literal constants.
2451/// Returns false if any leaf is neither a self-call(param-1) nor an integer literal.
2452fn cf_analyze_add_tree(
2453    expr: &Expr,
2454    func_name: Symbol,
2455    param_sym: Symbol,
2456    self_calls: &mut usize,
2457    constant_sum: &mut i64,
2458) -> bool {
2459    use crate::ast::stmt::BinaryOpKind;
2460    match expr {
2461        Expr::BinaryOp { op: BinaryOpKind::Add, left, right } => {
2462            cf_analyze_add_tree(left, func_name, param_sym, self_calls, constant_sum)
2463                && cf_analyze_add_tree(right, func_name, param_sym, self_calls, constant_sum)
2464        }
2465        _ if cf_is_self_call_with_decrement(expr, func_name, param_sym) => {
2466            *self_calls += 1;
2467            true
2468        }
2469        _ => {
2470            if let Some(n) = cf_extract_literal_int(expr) {
2471                *constant_sum += n;
2472                true
2473            } else {
2474                false
2475            }
2476        }
2477    }
2478}
2479
2480// =============================================================================
2481// O3: small fixed-size Seq scalarization (`Seq` → `[T; N]`).
2482// =============================================================================
2483
2484/// A `Seq` local proven to have a compile-time-constant size, built by
2485/// straight-line pushes and thereafter only indexed/length-queried, never
2486/// resized, aliased, or escaped. Codegen emits it as a Rust `[T; N]` array —
2487/// C's representation for nbody's bodies: stack-allocated, statically bounded.
2488pub(crate) struct ScalarizableSeq {
2489    pub elem_ty: String,
2490    pub len: usize,
2491}
2492
2493struct ScalarCand {
2494    elem_ty: String,
2495    len: usize,
2496    /// A non-push use of the handle has been seen — later pushes disqualify.
2497    seen_use: bool,
2498    disqualified: bool,
2499}
2500
2501/// `a new Seq of {Int|Nat|Float|Bool}` → the Rust element type, else None.
2502fn scalar_seq_elem_ty(value: &Expr, interner: &Interner) -> Option<String> {
2503    if let Expr::New { type_name, type_args, .. } = value {
2504        if matches!(interner.resolve(*type_name), "Seq" | "List") && type_args.len() == 1 {
2505            if let TypeExpr::Primitive(t) | TypeExpr::Named(t) = &type_args[0] {
2506                return match interner.resolve(*t) {
2507                    "Int" | "Nat" => Some("i64".to_string()),
2508                    "Float" => Some("f64".to_string()),
2509                    "Bool" => Some("bool".to_string()),
2510                    _ => None,
2511                };
2512            }
2513        }
2514    }
2515    None
2516}
2517
2518/// Maximum scalarized array length (keeps generated arrays small).
2519const MAX_SCALARIZE_LEN: usize = 64;
2520
2521/// Find scalarizable Seqs among the given block's locals (v1: the block
2522/// passed is Main; function bodies are not scanned). Conservative: any
2523/// appearance of a candidate outside an allowed position disqualifies it.
2524pub(crate) fn collect_scalarizable_seqs(
2525    stmts: &[Stmt],
2526    interner: &Interner,
2527) -> HashMap<Symbol, ScalarizableSeq> {
2528    // Kill switch for A/B measurement (`LOGOS_SCALARIZE=0`).
2529    if !crate::optimize::active_config().is_on(crate::optimization::Opt::Scalarize) {
2530        return HashMap::new();
2531    }
2532    let mut cand: HashMap<Symbol, ScalarCand> = HashMap::new();
2533    scalar_walk_block(stmts, true, &mut cand, interner);
2534    let result: HashMap<Symbol, ScalarizableSeq> = cand
2535        .into_iter()
2536        .filter_map(|(sym, c)| {
2537            if c.disqualified || c.len == 0 || c.len > MAX_SCALARIZE_LEN {
2538                None
2539            } else {
2540                Some((sym, ScalarizableSeq { elem_ty: c.elem_ty, len: c.len }))
2541            }
2542        })
2543        .collect();
2544    if !result.is_empty() {
2545        crate::optimize::mark_fired(crate::optimization::Opt::Scalarize);
2546    }
2547    result
2548}
2549
2550/// The set of Seq variables that qualify for fixed-size scalarization (`[T; N]`).
2551///
2552/// Exposed to the AOT loop-unroller (`optimize::unroll`): it only unrolls a loop
2553/// when every collection the loop indexes is one of these. Unrolling a loop over
2554/// a reference-semantics `LogosSeq` (a push-grown buffer or DP row) would destroy
2555/// the loop shapes the de-Rc buffer-reuse and borrow-hoist passes pattern-match
2556/// on — and yields no SROA benefit there anyway, since only scalarized arrays
2557/// promote to registers.
2558pub(crate) fn scalarizable_seq_symbols(
2559    stmts: &[Stmt],
2560    interner: &Interner,
2561) -> std::collections::HashSet<Symbol> {
2562    collect_scalarizable_seqs(stmts, interner)
2563        .into_keys()
2564        .collect()
2565}
2566
2567fn scalar_disq(cand: &mut HashMap<Symbol, ScalarCand>, sym: Symbol) {
2568    if let Some(c) = cand.get_mut(&sym) {
2569        c.disqualified = true;
2570    }
2571}
2572
2573// =============================================================================
2574// Co-indexed array interleaving (struct-of-arrays → array-of-structs).
2575// =============================================================================
2576
2577/// A group of same-type, same-length scalarizable Seqs built by an interleaved
2578/// (round-robin) push pattern — the columns of an array-of-structs. Codegen
2579/// fuses them into one `[[T; W]; N]` backing so per-entity fields are adjacent
2580/// (C's `struct Body[N]` layout), letting LLVM pack them with `movupd` instead
2581/// of gathering separate arrays with shuffles.
2582pub(super) struct InterleaveGroup {
2583    /// Members in column order: column `c` is `members[c]`.
2584    pub members: Vec<Symbol>,
2585    /// `N` — the common length (number of round-robin rounds).
2586    pub len: usize,
2587    /// The common Rust element type ("f64" / "i64" / "bool").
2588    pub elem_ty: String,
2589}
2590
2591/// Detect co-indexed array groups for AoS interleaving. GENERAL over W (number
2592/// of co-indexed arrays) and N (length). v1 recognizes a single round-robin
2593/// cycle that covers all scalarizable pushes, with W >= 2 distinct members of
2594/// equal element type and length — the canonical AoS-init pattern
2595/// (`Push x to ax; Push y to ay; …` repeated per entity).
2596pub(super) fn collect_interleaved_groups(
2597    stmts: &[Stmt],
2598    scalarizable: &HashMap<Symbol, ScalarizableSeq>,
2599    _interner: &Interner,
2600) -> Vec<InterleaveGroup> {
2601    // Kill switch for A/B measurement (`LOGOS_AOS=0`).
2602    if !crate::optimize::active_config().is_on(crate::optimization::Opt::Interleave) {
2603        return Vec::new();
2604    }
2605    if scalarizable.len() < 2 {
2606        return Vec::new();
2607    }
2608    // Ordered sequence of top-level pushes targeting scalarizable Seqs (their
2609    // pushes are straight-line top-level by the scalarization rule).
2610    let mut push_seq: Vec<Symbol> = Vec::new();
2611    for s in stmts {
2612        if let Stmt::Push { collection, .. } = s {
2613            if let Expr::Identifier(sym) = collection {
2614                if scalarizable.contains_key(sym) {
2615                    push_seq.push(*sym);
2616                }
2617            }
2618        }
2619    }
2620    if push_seq.len() < 2 {
2621        return Vec::new();
2622    }
2623    // Period W = index of the first repeat of the first pushed array.
2624    let first = push_seq[0];
2625    let w = match push_seq.iter().skip(1).position(|&s| s == first) {
2626        Some(p) => p + 1,
2627        None => return Vec::new(),
2628    };
2629    if w < 2 || push_seq.len() % w != 0 {
2630        return Vec::new();
2631    }
2632    let group: Vec<Symbol> = push_seq[..w].to_vec();
2633    // The W columns must be distinct symbols.
2634    let mut seen = std::collections::HashSet::new();
2635    if !group.iter().all(|s| seen.insert(*s)) {
2636        return Vec::new();
2637    }
2638    // Every round must repeat the same column order exactly.
2639    let rounds = push_seq.len() / w;
2640    for r in 0..rounds {
2641        for c in 0..w {
2642            if push_seq[r * w + c] != group[c] {
2643                return Vec::new();
2644            }
2645        }
2646    }
2647    // The group must cover the whole scalarizable set (no stragglers), and all
2648    // members must share element type and length (== rounds).
2649    if group.len() != scalarizable.len() {
2650        return Vec::new();
2651    }
2652    let elem_ty = scalarizable[&group[0]].elem_ty.clone();
2653    for m in &group {
2654        let info = &scalarizable[m];
2655        if info.elem_ty != elem_ty || info.len != rounds {
2656            return Vec::new();
2657        }
2658    }
2659    // Regime gate: AoS pays off only while the group stays MEMORY-RESIDENT —
2660    // accessed by variable indices in rolled loops, where adjacent fields load
2661    // packed (C's `movupd`) and beat SoA's gather. If ANY member is read/written
2662    // at a CONSTANT literal index, the kernel has been unrolled (or will SROA
2663    // into registers), where the layout is moot and SoA+unroll wins outright —
2664    // AoS there only perturbs LLVM's SLP for the worse (measured: nbody
2665    // 1.12×→1.19×). Leave those to the unroller.
2666    // `LOGOS_AOS_FORCE=1` bypasses the regime gate for A/B measurement.
2667    let force = std::env::var("LOGOS_AOS_FORCE").map(|v| v == "1").unwrap_or(false);
2668    let members: std::collections::HashSet<Symbol> = group.iter().copied().collect();
2669    if !force && block_has_const_member_index(stmts, &members) {
2670        // A const-index access means the kernel has been unrolled / will SROA into
2671        // registers — Unroll/Scalarize claim it, preempting AoS interleaving.
2672        crate::optimize::mark_preempted(
2673            crate::optimization::Opt::Scalarize,
2674            crate::optimization::Opt::Interleave,
2675        );
2676        crate::optimize::mark_preempted(
2677            crate::optimization::Opt::Unroll,
2678            crate::optimization::Opt::Interleave,
2679        );
2680        return Vec::new();
2681    }
2682    crate::optimize::mark_fired(crate::optimization::Opt::Interleave);
2683    vec![InterleaveGroup { members: group, len: rounds, elem_ty }]
2684}
2685
2686/// True if any member of `members` is indexed by a constant literal anywhere in
2687/// `stmts` (evidence the kernel unrolled / will SROA — the register regime).
2688fn block_has_const_member_index(stmts: &[Stmt], members: &std::collections::HashSet<Symbol>) -> bool {
2689    stmts.iter().any(|s| stmt_has_const_member_index(s, members))
2690}
2691
2692fn is_member_const_index(collection: &Expr, index: &Expr, members: &std::collections::HashSet<Symbol>) -> bool {
2693    matches!(collection, Expr::Identifier(s) if members.contains(s))
2694        && matches!(index, Expr::Literal(Literal::Number(_)))
2695}
2696
2697fn stmt_has_const_member_index(s: &Stmt, members: &std::collections::HashSet<Symbol>) -> bool {
2698    match s {
2699        Stmt::SetIndex { collection, index, value } => {
2700            is_member_const_index(collection, index, members)
2701                || expr_has_const_member_index(collection, members)
2702                || expr_has_const_member_index(index, members)
2703                || expr_has_const_member_index(value, members)
2704        }
2705        Stmt::Let { value, .. } | Stmt::Set { value, .. } => expr_has_const_member_index(value, members),
2706        Stmt::Push { value, collection } => {
2707            expr_has_const_member_index(value, members) || expr_has_const_member_index(collection, members)
2708        }
2709        Stmt::Show { object, .. } => expr_has_const_member_index(object, members),
2710        Stmt::SetField { object, value, .. } => {
2711            expr_has_const_member_index(object, members) || expr_has_const_member_index(value, members)
2712        }
2713        Stmt::Give { object, recipient } => {
2714            expr_has_const_member_index(object, members) || expr_has_const_member_index(recipient, members)
2715        }
2716        Stmt::Add { value, collection } | Stmt::Remove { value, collection } => {
2717            expr_has_const_member_index(value, members) || expr_has_const_member_index(collection, members)
2718        }
2719        Stmt::RuntimeAssert { condition, .. } => expr_has_const_member_index(condition, members),
2720        Stmt::Return { value } => matches!(value, Some(v) if expr_has_const_member_index(v, members)),
2721        Stmt::Call { args, .. } => args.iter().any(|a| expr_has_const_member_index(a, members)),
2722        Stmt::If { cond, then_block, else_block } => {
2723            expr_has_const_member_index(cond, members)
2724                || block_has_const_member_index(then_block, members)
2725                || matches!(else_block, Some(eb) if block_has_const_member_index(eb, members))
2726        }
2727        Stmt::While { cond, body, .. } => {
2728            expr_has_const_member_index(cond, members) || block_has_const_member_index(body, members)
2729        }
2730        Stmt::Repeat { iterable, body, .. } => {
2731            expr_has_const_member_index(iterable, members) || block_has_const_member_index(body, members)
2732        }
2733        Stmt::Inspect { target, arms, .. } => {
2734            expr_has_const_member_index(target, members)
2735                || arms.iter().any(|a| block_has_const_member_index(a.body, members))
2736        }
2737        _ => false,
2738    }
2739}
2740
2741fn expr_has_const_member_index(e: &Expr, members: &std::collections::HashSet<Symbol>) -> bool {
2742    match e {
2743        Expr::Index { collection, index } => {
2744            is_member_const_index(collection, index, members)
2745                || expr_has_const_member_index(collection, members)
2746                || expr_has_const_member_index(index, members)
2747        }
2748        Expr::Slice { collection, start, end } => {
2749            expr_has_const_member_index(collection, members)
2750                || expr_has_const_member_index(start, members)
2751                || expr_has_const_member_index(end, members)
2752        }
2753        Expr::BinaryOp { left, right, .. } => {
2754            expr_has_const_member_index(left, members) || expr_has_const_member_index(right, members)
2755        }
2756        Expr::Not { operand } => expr_has_const_member_index(operand, members),
2757        Expr::Length { collection } => expr_has_const_member_index(collection, members),
2758        Expr::Contains { collection, value } => {
2759            expr_has_const_member_index(collection, members) || expr_has_const_member_index(value, members)
2760        }
2761        Expr::Union { left, right } | Expr::Intersection { left, right } => {
2762            expr_has_const_member_index(left, members) || expr_has_const_member_index(right, members)
2763        }
2764        Expr::Call { args, .. } | Expr::CallExpr { args, .. } => {
2765            args.iter().any(|a| expr_has_const_member_index(a, members))
2766        }
2767        Expr::FieldAccess { object, .. } => expr_has_const_member_index(object, members),
2768        Expr::Copy { expr } => expr_has_const_member_index(expr, members),
2769        Expr::Give { value } | Expr::OptionSome { value } => expr_has_const_member_index(value, members),
2770        Expr::WithCapacity { value, capacity } => {
2771            expr_has_const_member_index(value, members) || expr_has_const_member_index(capacity, members)
2772        }
2773        Expr::Range { start, end } => {
2774            expr_has_const_member_index(start, members) || expr_has_const_member_index(end, members)
2775        }
2776        Expr::List(items) | Expr::Tuple(items) => {
2777            items.iter().any(|i| expr_has_const_member_index(i, members))
2778        }
2779        Expr::InterpolatedString(parts) => parts.iter().any(|p| match p {
2780            crate::ast::stmt::StringPart::Expr { value, .. } => expr_has_const_member_index(value, members),
2781            _ => false,
2782        }),
2783        _ => false,
2784    }
2785}
2786
2787/// A parsed `__aos:<backing>:<col>:<width>:<len>:<elem>` member type tag — one
2788/// column of a fused array-of-structs. `item i of member` lowers to
2789/// `backing[(i-1)][col]`; column 0 emits the `[[elem; width]; len]` backing.
2790pub(super) struct AosTag {
2791    pub backing: String,
2792    pub col: usize,
2793    pub width: usize,
2794    pub len: usize,
2795    pub elem: String,
2796}
2797
2798/// Parse an AoS member type tag (registered in `codegen_program`). The backing
2799/// name is the first member's emitted identifier; it contains no `:`, and the
2800/// element type (`f64`/`i64`/`bool`) contains none either, so `:`-splitting is
2801/// unambiguous.
2802pub(super) fn parse_aos_tag(ty: Option<&String>) -> Option<AosTag> {
2803    let rest = ty?.strip_prefix("__aos:")?;
2804    let mut parts = rest.split(':');
2805    let backing = parts.next()?.to_string();
2806    let col = parts.next()?.parse().ok()?;
2807    let width = parts.next()?.parse().ok()?;
2808    let len = parts.next()?.parse().ok()?;
2809    let elem = parts.next()?.to_string();
2810    Some(AosTag { backing, col, width, len, elem })
2811}
2812
2813fn scalar_mark_use(cand: &mut HashMap<Symbol, ScalarCand>, sym: Symbol) {
2814    if let Some(c) = cand.get_mut(&sym) {
2815        c.seen_use = true;
2816    }
2817}
2818
2819/// A collection-position expression: a bare candidate handle is an ALLOWED
2820/// access (read/length); anything else is a value.
2821fn scalar_note_access(e: &Expr, cand: &mut HashMap<Symbol, ScalarCand>) {
2822    if let Expr::Identifier(s) = e {
2823        scalar_mark_use(cand, *s);
2824    } else {
2825        scalar_note_value(e, cand);
2826    }
2827}
2828
2829/// A value-position expression: a bare candidate handle DISQUALIFIES (the
2830/// handle itself flows somewhere). `item i of x` and `length of x` remain
2831/// allowed accesses — their results are scalars.
2832fn scalar_note_value(e: &Expr, cand: &mut HashMap<Symbol, ScalarCand>) {
2833    match e {
2834        Expr::Identifier(s) => scalar_disq(cand, *s),
2835        Expr::Literal(_) | Expr::OptionNone => {}
2836        Expr::Index { collection, index } => {
2837            scalar_note_access(collection, cand);
2838            scalar_note_value(index, cand);
2839        }
2840        Expr::Length { collection } => scalar_note_access(collection, cand),
2841        Expr::Slice { collection, start, end } => {
2842            // A slice yields a new Seq — the handle escapes into it.
2843            if let Expr::Identifier(s) = collection {
2844                scalar_disq(cand, *s);
2845            } else {
2846                scalar_note_value(collection, cand);
2847            }
2848            scalar_note_value(start, cand);
2849            scalar_note_value(end, cand);
2850        }
2851        Expr::Contains { collection, value } => {
2852            if let Expr::Identifier(s) = collection {
2853                scalar_disq(cand, *s);
2854            } else {
2855                scalar_note_value(collection, cand);
2856            }
2857            scalar_note_value(value, cand);
2858        }
2859        Expr::Copy { expr } => {
2860            if let Expr::Identifier(s) = expr {
2861                scalar_disq(cand, *s);
2862            } else {
2863                scalar_note_value(expr, cand);
2864            }
2865        }
2866        Expr::BinaryOp { left, right, .. }
2867        | Expr::Union { left, right }
2868        | Expr::Intersection { left, right }
2869        | Expr::Range { start: left, end: right } => {
2870            scalar_note_value(left, cand);
2871            scalar_note_value(right, cand);
2872        }
2873        Expr::Not { operand } => scalar_note_value(operand, cand),
2874        Expr::FieldAccess { object, .. } => scalar_note_value(object, cand),
2875        Expr::OptionSome { value } | Expr::Give { value } => scalar_note_value(value, cand),
2876        Expr::WithCapacity { value, capacity } => {
2877            scalar_note_value(value, cand);
2878            scalar_note_value(capacity, cand);
2879        }
2880        Expr::List(items) | Expr::Tuple(items) => {
2881            for it in items {
2882                scalar_note_value(it, cand);
2883            }
2884        }
2885        Expr::New { init_fields, .. } => {
2886            for (_, v) in init_fields {
2887                scalar_note_value(v, cand);
2888            }
2889        }
2890        Expr::NewVariant { fields, .. } => {
2891            for (_, v) in fields {
2892                scalar_note_value(v, cand);
2893            }
2894        }
2895        Expr::Call { args, .. } => {
2896            for a in args {
2897                scalar_note_value(a, cand);
2898            }
2899        }
2900        Expr::CallExpr { callee, args } => {
2901            scalar_note_value(callee, cand);
2902            for a in args {
2903                scalar_note_value(a, cand);
2904            }
2905        }
2906        Expr::InterpolatedString(parts) => {
2907            for p in parts {
2908                if let crate::ast::stmt::StringPart::Expr { value, .. } = p {
2909                    scalar_note_value(value, cand);
2910                }
2911            }
2912        }
2913        Expr::ManifestOf { zone } => scalar_note_value(zone, cand),
2914        Expr::ChunkAt { index, zone } => {
2915            scalar_note_value(index, cand);
2916            scalar_note_value(zone, cand);
2917        }
2918        // Closures and escape hatches are opaque — they may capture or
2919        // reference any handle in scope. Conservatively disqualify every
2920        // candidate rather than risk scalarizing a captured Seq.
2921        Expr::Closure { .. } | Expr::Escape { .. } => {
2922            for c in cand.values_mut() {
2923                c.disqualified = true;
2924            }
2925        }
2926        _ => {}
2927    }
2928}
2929
2930fn scalar_walk_block(
2931    stmts: &[Stmt],
2932    top_level: bool,
2933    cand: &mut HashMap<Symbol, ScalarCand>,
2934    interner: &Interner,
2935) {
2936    for stmt in stmts {
2937        match stmt {
2938            Stmt::Let { var, value, mutable, .. } => {
2939                if top_level && *mutable {
2940                    if let Some(elem) = scalar_seq_elem_ty(value, interner) {
2941                        cand.insert(
2942                            *var,
2943                            ScalarCand { elem_ty: elem, len: 0, seen_use: false, disqualified: false },
2944                        );
2945                        continue;
2946                    }
2947                }
2948                // `Let y be x` aliases x; any candidate in the value escapes.
2949                if let Expr::Identifier(s) = value {
2950                    scalar_disq(cand, *s);
2951                } else {
2952                    scalar_note_value(value, cand);
2953                }
2954            }
2955            Stmt::Push { value, collection } => {
2956                // The pushed value may carry a candidate handle (it escapes).
2957                if let Expr::Identifier(s) = value {
2958                    scalar_disq(cand, *s);
2959                } else {
2960                    scalar_note_value(value, cand);
2961                }
2962                if let Expr::Identifier(x) = collection {
2963                    let nested_or_used = {
2964                        match cand.get(x) {
2965                            Some(c) => !top_level || c.seen_use || c.disqualified,
2966                            None => false,
2967                        }
2968                    };
2969                    if cand.contains_key(x) {
2970                        if nested_or_used {
2971                            scalar_disq(cand, *x);
2972                        } else if let Some(c) = cand.get_mut(x) {
2973                            c.len += 1;
2974                        }
2975                    }
2976                } else {
2977                    scalar_note_value(collection, cand);
2978                }
2979            }
2980            Stmt::SetIndex { collection, index, value } => {
2981                if let Expr::Identifier(x) = collection {
2982                    scalar_mark_use(cand, *x);
2983                } else {
2984                    scalar_note_value(collection, cand);
2985                }
2986                scalar_note_value(index, cand);
2987                scalar_note_value(value, cand);
2988            }
2989            Stmt::Set { target, value } => {
2990                // Rebinding a candidate disqualifies it; aliasing escapes the RHS.
2991                scalar_disq(cand, *target);
2992                if let Expr::Identifier(s) = value {
2993                    scalar_disq(cand, *s);
2994                } else {
2995                    scalar_note_value(value, cand);
2996                }
2997            }
2998            Stmt::Pop { collection, .. } => {
2999                if let Expr::Identifier(x) = collection {
3000                    scalar_disq(cand, *x);
3001                } else {
3002                    scalar_note_value(collection, cand);
3003                }
3004            }
3005            Stmt::Add { collection, value } | Stmt::Remove { collection, value } => {
3006                if let Expr::Identifier(x) = collection {
3007                    scalar_disq(cand, *x);
3008                } else {
3009                    scalar_note_value(collection, cand);
3010                }
3011                scalar_note_value(value, cand);
3012            }
3013            Stmt::Show { object, recipient } => {
3014                scalar_note_value(object, cand);
3015                scalar_note_value(recipient, cand);
3016            }
3017            Stmt::Give { object, recipient } => {
3018                if let Expr::Identifier(s) = object {
3019                    scalar_disq(cand, *s);
3020                } else {
3021                    scalar_note_value(object, cand);
3022                }
3023                scalar_note_value(recipient, cand);
3024            }
3025            Stmt::Return { value: Some(v) } => {
3026                if let Expr::Identifier(s) = v {
3027                    scalar_disq(cand, *s);
3028                } else {
3029                    scalar_note_value(v, cand);
3030                }
3031            }
3032            Stmt::SetField { object, value, .. } => {
3033                scalar_note_value(object, cand);
3034                scalar_note_value(value, cand);
3035            }
3036            Stmt::If { cond, then_block, else_block } => {
3037                scalar_note_value(cond, cand);
3038                scalar_walk_block(then_block, false, cand, interner);
3039                if let Some(eb) = else_block {
3040                    scalar_walk_block(eb, false, cand, interner);
3041                }
3042            }
3043            Stmt::While { cond, body, .. } => {
3044                scalar_note_value(cond, cand);
3045                scalar_walk_block(body, false, cand, interner);
3046            }
3047            Stmt::Repeat { iterable, body, .. } => {
3048                if let Expr::Identifier(x) = iterable {
3049                    scalar_disq(cand, *x);
3050                } else {
3051                    scalar_note_value(iterable, cand);
3052                }
3053                scalar_walk_block(body, false, cand, interner);
3054            }
3055            Stmt::Inspect { target, arms, .. } => {
3056                scalar_note_value(target, cand);
3057                for arm in arms {
3058                    scalar_walk_block(&arm.body, false, cand, interner);
3059                }
3060            }
3061            Stmt::RuntimeAssert { condition, .. } => scalar_note_value(condition, cand),
3062            Stmt::Call { args, .. } => {
3063                for a in args {
3064                    if let Expr::Identifier(s) = a {
3065                        scalar_disq(cand, *s);
3066                    } else {
3067                        scalar_note_value(a, cand);
3068                    }
3069                }
3070            }
3071            Stmt::Zone { body, .. } => scalar_walk_block(body, false, cand, interner),
3072            Stmt::Concurrent { tasks } | Stmt::Parallel { tasks } => {
3073                scalar_walk_block(tasks, false, cand, interner)
3074            }
3075            // FunctionDef and other forms: candidates are Main-locals; a
3076            // function body cannot reference them. Nothing to do.
3077            _ => {}
3078        }
3079    }
3080}