Skip to main content

nxpu_opt/
memory.rs

1//! Memory planning and buffer allocation pass.
2//!
3//! Analyzes tensor lifetimes within IR functions and assigns buffer offsets
4//! using a greedy allocator. When lifetimes do not overlap, the allocator
5//! reuses buffer regions, reducing peak memory usage.
6
7use std::collections::HashMap;
8
9use nxpu_ir::{
10    ArraySize, Expression, Function, GlobalVariable, Handle, Module, Statement, TypeInner,
11};
12
13// Re-export the canonical types from nxpu-backend-core.
14pub use nxpu_backend_core::{BufferAllocation, MemoryPlan, TensorId};
15
16use crate::Pass;
17
18// ---------------------------------------------------------------------------
19// Public types (only LiveInterval is defined here; the rest come from
20// nxpu-backend-core)
21// ---------------------------------------------------------------------------
22
23/// The liveness interval (first-use to last-use) of a tensor.
24#[derive(Clone, Debug, PartialEq, Eq)]
25pub struct LiveInterval {
26    /// Index of the first statement that uses this tensor.
27    pub start: usize,
28    /// Index of the last statement that uses this tensor.
29    pub end: usize,
30    /// Size of the tensor in bytes.
31    pub size_bytes: usize,
32}
33
34// ---------------------------------------------------------------------------
35// Tensor classification
36// ---------------------------------------------------------------------------
37
38/// Information about a tensor discovered during analysis.
39#[derive(Clone, Debug)]
40struct TensorInfo {
41    id: TensorId,
42    /// Human-readable name (from the IR variable, if any).
43    #[allow(dead_code)]
44    name: Option<String>,
45    /// Size in bytes (0 if dynamic / unknown).
46    size_bytes: usize,
47}
48
49// ---------------------------------------------------------------------------
50// Size computation
51// ---------------------------------------------------------------------------
52
53/// Compute the byte size of a type. Returns 0 for dynamic / unsized types.
54fn type_size_bytes(module: &Module, ty: Handle<nxpu_ir::Type>) -> usize {
55    match &module.types[ty].inner {
56        TypeInner::Scalar(s) => s.width as usize,
57        TypeInner::Vector { size, scalar } => (*size as usize) * (scalar.width as usize),
58        TypeInner::Matrix {
59            columns,
60            rows,
61            scalar,
62        } => (*columns as usize) * (*rows as usize) * (scalar.width as usize),
63        TypeInner::Atomic(s) => s.width as usize,
64        TypeInner::Pointer { .. } => 0, // pointers have no data size for planning
65        TypeInner::Array { base, size, stride } => match size {
66            ArraySize::Constant(n) => {
67                let elem = type_size_bytes(module, *base);
68                if elem == 0 {
69                    (*n as usize) * (*stride as usize)
70                } else {
71                    (*n as usize) * elem
72                }
73            }
74            ArraySize::Dynamic => 0, // runtime-sized
75        },
76        TypeInner::Struct { span, .. } => *span as usize,
77        TypeInner::Tensor { scalar, shape } => {
78            if shape.is_fully_static() {
79                let elem_count: usize = shape
80                    .dims
81                    .iter()
82                    .map(|d| match d {
83                        nxpu_ir::Dimension::Fixed(n) => *n as usize,
84                        nxpu_ir::Dimension::Dynamic(_) | nxpu_ir::Dimension::Symbolic(_) => 0,
85                    })
86                    .product();
87                elem_count * (scalar.width as usize)
88            } else {
89                0
90            }
91        }
92    }
93}
94
95// ---------------------------------------------------------------------------
96// Lifetime analysis
97// ---------------------------------------------------------------------------
98
99/// For an expression, find which global/local variable it ultimately refers to
100/// and return the corresponding `TensorId`.
101fn resolve_expr_tensor_id(
102    handle: Handle<Expression>,
103    func: &Function,
104    global_id_map: &HashMap<Handle<GlobalVariable>, TensorId>,
105    local_id_map: &HashMap<Handle<nxpu_ir::LocalVariable>, TensorId>,
106) -> Option<TensorId> {
107    let expr = func.expressions.try_get(handle)?;
108    match expr {
109        Expression::GlobalVariable(gv) => global_id_map.get(gv).copied(),
110        Expression::LocalVariable(lv) => local_id_map.get(lv).copied(),
111        Expression::Load { pointer } => {
112            resolve_expr_tensor_id(*pointer, func, global_id_map, local_id_map)
113        }
114        Expression::Access { base, .. } | Expression::AccessIndex { base, .. } => {
115            resolve_expr_tensor_id(*base, func, global_id_map, local_id_map)
116        }
117        _ => None,
118    }
119}
120
121/// Try to resolve an expression handle to a tensor ID and push it if found.
122fn try_resolve_push(
123    h: Handle<Expression>,
124    func: &Function,
125    global_id_map: &HashMap<Handle<GlobalVariable>, TensorId>,
126    local_id_map: &HashMap<Handle<nxpu_ir::LocalVariable>, TensorId>,
127    ids: &mut Vec<TensorId>,
128) {
129    if let Some(tid) = resolve_expr_tensor_id(h, func, global_id_map, local_id_map) {
130        ids.push(tid);
131    }
132}
133
134/// Collect all tensor IDs referenced by a statement, recursing into
135/// sub-blocks (If/Loop bodies).
136fn collect_stmt_tensor_ids(
137    stmt: &Statement,
138    func: &Function,
139    global_id_map: &HashMap<Handle<GlobalVariable>, TensorId>,
140    local_id_map: &HashMap<Handle<nxpu_ir::LocalVariable>, TensorId>,
141) -> Vec<TensorId> {
142    let mut ids = Vec::new();
143
144    match stmt {
145        Statement::Emit(range) => {
146            // For each expression in the emitted range, check if it references
147            // a variable (directly or through operands).
148            let idx_range = range.index_range();
149            for (expr_handle, _) in func.expressions.iter() {
150                if idx_range.contains(&(expr_handle.index() as u32)) {
151                    try_resolve_push(expr_handle, func, global_id_map, local_id_map, &mut ids);
152                }
153            }
154        }
155        Statement::Store { pointer, value } => {
156            try_resolve_push(*pointer, func, global_id_map, local_id_map, &mut ids);
157            try_resolve_push(*value, func, global_id_map, local_id_map, &mut ids);
158        }
159        Statement::If {
160            condition,
161            accept,
162            reject,
163        } => {
164            try_resolve_push(*condition, func, global_id_map, local_id_map, &mut ids);
165            for s in accept {
166                ids.extend(collect_stmt_tensor_ids(
167                    s,
168                    func,
169                    global_id_map,
170                    local_id_map,
171                ));
172            }
173            for s in reject {
174                ids.extend(collect_stmt_tensor_ids(
175                    s,
176                    func,
177                    global_id_map,
178                    local_id_map,
179                ));
180            }
181        }
182        Statement::Loop {
183            body,
184            continuing,
185            break_if,
186        } => {
187            for s in body {
188                ids.extend(collect_stmt_tensor_ids(
189                    s,
190                    func,
191                    global_id_map,
192                    local_id_map,
193                ));
194            }
195            for s in continuing {
196                ids.extend(collect_stmt_tensor_ids(
197                    s,
198                    func,
199                    global_id_map,
200                    local_id_map,
201                ));
202            }
203            if let Some(brk) = break_if {
204                try_resolve_push(*brk, func, global_id_map, local_id_map, &mut ids);
205            }
206        }
207        Statement::Call {
208            arguments, result, ..
209        } => {
210            for arg in arguments {
211                try_resolve_push(*arg, func, global_id_map, local_id_map, &mut ids);
212            }
213            if let Some(r) = result {
214                try_resolve_push(*r, func, global_id_map, local_id_map, &mut ids);
215            }
216        }
217        Statement::Atomic {
218            pointer,
219            value,
220            result,
221            fun,
222        } => {
223            try_resolve_push(*pointer, func, global_id_map, local_id_map, &mut ids);
224            try_resolve_push(*value, func, global_id_map, local_id_map, &mut ids);
225            if let Some(r) = result {
226                try_resolve_push(*r, func, global_id_map, local_id_map, &mut ids);
227            }
228            if let nxpu_ir::AtomicFunction::Exchange {
229                compare: Some(cmp), ..
230            } = fun
231            {
232                try_resolve_push(*cmp, func, global_id_map, local_id_map, &mut ids);
233            }
234        }
235        Statement::Return { value } => {
236            if let Some(v) = value {
237                try_resolve_push(*v, func, global_id_map, local_id_map, &mut ids);
238            }
239        }
240        Statement::Barrier(_) | Statement::Break | Statement::Continue => {}
241    }
242    ids
243}
244
245/// Analyze a single function and return liveness intervals for all tensors.
246fn analyze_function_lifetimes(
247    module: &Module,
248    func: &Function,
249    global_id_map: &HashMap<Handle<GlobalVariable>, TensorId>,
250    tensor_infos: &mut Vec<TensorInfo>,
251    next_id: &mut usize,
252) -> HashMap<TensorId, LiveInterval> {
253    // Build a local-variable -> TensorId mapping.
254    let mut local_id_map: HashMap<Handle<nxpu_ir::LocalVariable>, TensorId> = HashMap::new();
255    for (lv_handle, lv) in func.local_variables.iter() {
256        let tid = TensorId(*next_id);
257        *next_id += 1;
258        let size = type_size_bytes(module, lv.ty);
259        tensor_infos.push(TensorInfo {
260            id: tid,
261            name: lv.name.clone(),
262            size_bytes: size,
263        });
264        local_id_map.insert(lv_handle, tid);
265    }
266
267    let mut intervals: HashMap<TensorId, LiveInterval> = HashMap::new();
268
269    for (stmt_idx, stmt) in func.body.iter().enumerate() {
270        let tensor_ids = collect_stmt_tensor_ids(stmt, func, global_id_map, &local_id_map);
271        for tid in tensor_ids {
272            // Find the size from tensor_infos.
273            let size = tensor_infos
274                .iter()
275                .find(|t| t.id == tid)
276                .map(|t| t.size_bytes)
277                .unwrap_or(0);
278            intervals
279                .entry(tid)
280                .and_modify(|interval| {
281                    if stmt_idx < interval.start {
282                        interval.start = stmt_idx;
283                    }
284                    if stmt_idx > interval.end {
285                        interval.end = stmt_idx;
286                    }
287                })
288                .or_insert(LiveInterval {
289                    start: stmt_idx,
290                    end: stmt_idx,
291                    size_bytes: size,
292                });
293        }
294    }
295
296    intervals
297}
298
299// ---------------------------------------------------------------------------
300// Greedy buffer allocator
301// ---------------------------------------------------------------------------
302
303/// A region in the buffer that is currently in use.
304#[derive(Clone, Debug)]
305struct ActiveAllocation {
306    #[allow(dead_code)]
307    tensor_id: TensorId,
308    offset: usize,
309    size: usize,
310    end: usize, // last statement index where this tensor is live
311}
312
313/// Greedy first-fit allocator. Assigns offsets to tensors sorted by start time.
314/// When a tensor's lifetime ends, its region becomes available for reuse.
315fn greedy_allocate(intervals: &HashMap<TensorId, LiveInterval>) -> MemoryPlan {
316    if intervals.is_empty() {
317        return MemoryPlan::default();
318    }
319
320    // Sort intervals by start time, breaking ties by larger size first.
321    let mut sorted: Vec<(TensorId, &LiveInterval)> = intervals
322        .iter()
323        .map(|(tid, interval)| (*tid, interval))
324        .collect();
325    sorted.sort_by(|a, b| {
326        a.1.start
327            .cmp(&b.1.start)
328            .then(b.1.size_bytes.cmp(&a.1.size_bytes))
329    });
330
331    let mut active: Vec<ActiveAllocation> = Vec::new();
332    let mut allocations: Vec<BufferAllocation> = Vec::new();
333    let mut peak = 0usize;
334
335    for (tid, interval) in sorted {
336        // Skip zero-size tensors (dynamic / unknown).
337        if interval.size_bytes == 0 {
338            allocations.push(BufferAllocation {
339                tensor_id: tid,
340                offset: 0,
341                size_bytes: 0,
342            });
343            continue;
344        }
345
346        // Expire allocations whose lifetime has ended before this tensor starts.
347        active.retain(|a| a.end >= interval.start);
348
349        // Sort active allocations by offset for gap-finding.
350        active.sort_by_key(|a| a.offset);
351
352        // Find first gap that fits.
353        let mut offset = 0usize;
354        for a in &active {
355            if offset + interval.size_bytes <= a.offset {
356                break;
357            }
358            offset = a.offset + a.size;
359        }
360
361        let end_of_alloc = offset + interval.size_bytes;
362        if end_of_alloc > peak {
363            peak = end_of_alloc;
364        }
365
366        allocations.push(BufferAllocation {
367            tensor_id: tid,
368            offset,
369            size_bytes: interval.size_bytes,
370        });
371
372        active.push(ActiveAllocation {
373            tensor_id: tid,
374            offset,
375            size: interval.size_bytes,
376            end: interval.end,
377        });
378    }
379
380    MemoryPlan {
381        allocations,
382        peak_bytes: peak,
383    }
384}
385
386// ---------------------------------------------------------------------------
387// Public API
388// ---------------------------------------------------------------------------
389
390/// Analyze an IR module and produce a memory plan.
391///
392/// For each entry point (and helper function), determines tensor lifetimes
393/// and assigns buffer offsets using a greedy first-fit algorithm.
394pub fn plan_memory(module: &Module) -> MemoryPlan {
395    let mut next_id = 0usize;
396    let mut tensor_infos: Vec<TensorInfo> = Vec::new();
397    let mut global_id_map: HashMap<Handle<GlobalVariable>, TensorId> = HashMap::new();
398
399    // Register global variables as tensors.
400    for (handle, gv) in module.global_variables.iter() {
401        let tid = TensorId(next_id);
402        next_id += 1;
403        let size = type_size_bytes(module, gv.ty);
404        tensor_infos.push(TensorInfo {
405            id: tid,
406            name: gv.name.clone(),
407            size_bytes: size,
408        });
409        global_id_map.insert(handle, tid);
410    }
411
412    // Merge liveness intervals across all functions.
413    let mut all_intervals: HashMap<TensorId, LiveInterval> = HashMap::new();
414
415    // Analyze helper functions.
416    for (_, func) in module.functions.iter() {
417        let intervals = analyze_function_lifetimes(
418            module,
419            func,
420            &global_id_map,
421            &mut tensor_infos,
422            &mut next_id,
423        );
424        merge_intervals(&mut all_intervals, &intervals);
425    }
426
427    // Analyze entry points.
428    for ep in &module.entry_points {
429        let intervals = analyze_function_lifetimes(
430            module,
431            &ep.function,
432            &global_id_map,
433            &mut tensor_infos,
434            &mut next_id,
435        );
436        merge_intervals(&mut all_intervals, &intervals);
437    }
438
439    // Include global variables that weren't referenced in any function body
440    // (they still need space allocated).
441    for info in &tensor_infos {
442        if info.size_bytes > 0 && !all_intervals.contains_key(&info.id) {
443            // Give them a full-lifetime interval so they are always allocated.
444            all_intervals.insert(
445                info.id,
446                LiveInterval {
447                    start: 0,
448                    end: usize::MAX,
449                    size_bytes: info.size_bytes,
450                },
451            );
452        }
453    }
454
455    greedy_allocate(&all_intervals)
456}
457
458/// Merge `src` intervals into `dst`, extending existing intervals as needed.
459fn merge_intervals(
460    dst: &mut HashMap<TensorId, LiveInterval>,
461    src: &HashMap<TensorId, LiveInterval>,
462) {
463    for (tid, interval) in src {
464        dst.entry(*tid)
465            .and_modify(|existing| {
466                if interval.start < existing.start {
467                    existing.start = interval.start;
468                }
469                if interval.end > existing.end {
470                    existing.end = interval.end;
471                }
472            })
473            .or_insert_with(|| interval.clone());
474    }
475}
476
477// ---------------------------------------------------------------------------
478// Pass integration
479// ---------------------------------------------------------------------------
480
481/// Memory planning analysis pass.
482///
483/// This pass does not modify the IR; it only computes and logs a memory plan.
484/// Callers should use [`plan_memory`] directly to obtain the plan.
485#[derive(Debug)]
486pub struct MemoryPlanning;
487
488impl Pass for MemoryPlanning {
489    fn name(&self) -> &str {
490        "MemoryPlanning"
491    }
492
493    fn run(&self, module: &mut Module) -> bool {
494        let plan = plan_memory(module);
495        log::debug!(
496            "memory plan: {} allocations, peak {} bytes",
497            plan.allocations.len(),
498            plan.peak_bytes
499        );
500        // Analysis pass -- never modifies the IR.
501        false
502    }
503}
504
505// ---------------------------------------------------------------------------
506// Tests
507// ---------------------------------------------------------------------------
508
509#[cfg(test)]
510mod tests {
511    use super::*;
512    use nxpu_ir::*;
513
514    // Helper: create a type handle for f32.
515    fn f32_type(module: &mut Module) -> Handle<Type> {
516        module.types.insert(Type {
517            name: None,
518            inner: TypeInner::Scalar(Scalar::F32),
519        })
520    }
521
522    // Helper: create a fixed-size f32 array type.
523    fn f32_array_type(module: &mut Module, count: u32) -> Handle<Type> {
524        let f32_ty = f32_type(module);
525        module.types.insert(Type {
526            name: None,
527            inner: TypeInner::Array {
528                base: f32_ty,
529                size: ArraySize::Constant(count),
530                stride: 4,
531            },
532        })
533    }
534
535    // Helper: create a dynamic f32 array type.
536    fn f32_dynamic_array_type(module: &mut Module) -> Handle<Type> {
537        let f32_ty = f32_type(module);
538        module.types.insert(Type {
539            name: None,
540            inner: TypeInner::Array {
541                base: f32_ty,
542                size: ArraySize::Dynamic,
543                stride: 4,
544            },
545        })
546    }
547
548    #[test]
549    fn empty_module_produces_empty_plan() {
550        let module = Module::default();
551        let plan = plan_memory(&module);
552        assert_eq!(plan.allocations.len(), 0);
553        assert_eq!(plan.peak_bytes, 0);
554    }
555
556    #[test]
557    fn single_tensor() {
558        let mut module = Module::default();
559        let arr_ty = f32_array_type(&mut module, 256); // 256 * 4 = 1024 bytes
560
561        let gv = module.global_variables.append(GlobalVariable {
562            name: Some("buf".into()),
563            space: AddressSpace::Storage {
564                access: StorageAccess::LOAD | StorageAccess::STORE,
565            },
566            binding: Some(ResourceBinding {
567                group: 0,
568                binding: 0,
569            }),
570            ty: arr_ty,
571            init: None,
572            layout: None,
573        });
574
575        // Create an entry point that references the global.
576        let mut func = Function::new("main");
577        let ptr = func.expressions.append(Expression::GlobalVariable(gv));
578        let lit = func
579            .expressions
580            .append(Expression::Literal(Literal::F32(1.0)));
581        func.body.push(Statement::Store {
582            pointer: ptr,
583            value: lit,
584        });
585
586        module.entry_points.push(EntryPoint {
587            name: "main".into(),
588            workgroup_size: [1, 1, 1],
589            function: func,
590        });
591
592        let plan = plan_memory(&module);
593        assert_eq!(plan.allocations.len(), 1);
594        assert_eq!(plan.allocations[0].size_bytes, 1024);
595        assert_eq!(plan.peak_bytes, 1024);
596    }
597
598    #[test]
599    fn non_overlapping_lifetimes_reuse_buffer() {
600        // Two local tensors: one used in stmt 0, another used in stmt 1.
601        // They should share the same buffer region.
602        let mut module = Module::default();
603        let arr_ty = f32_array_type(&mut module, 128); // 512 bytes each
604
605        let mut func = Function::new("main");
606
607        // Local variable A (used only in first Store).
608        let lv_a = func.local_variables.append(LocalVariable {
609            name: Some("temp_a".into()),
610            ty: arr_ty,
611            init: None,
612        });
613        // Local variable B (used only in second Store).
614        let lv_b = func.local_variables.append(LocalVariable {
615            name: Some("temp_b".into()),
616            ty: arr_ty,
617            init: None,
618        });
619
620        let ptr_a = func.expressions.append(Expression::LocalVariable(lv_a));
621        let ptr_b = func.expressions.append(Expression::LocalVariable(lv_b));
622        let lit = func
623            .expressions
624            .append(Expression::Literal(Literal::F32(0.0)));
625
626        // stmt 0: store to A
627        func.body.push(Statement::Store {
628            pointer: ptr_a,
629            value: lit,
630        });
631        // stmt 1: store to B
632        func.body.push(Statement::Store {
633            pointer: ptr_b,
634            value: lit,
635        });
636
637        module.entry_points.push(EntryPoint {
638            name: "main".into(),
639            workgroup_size: [1, 1, 1],
640            function: func,
641        });
642
643        let plan = plan_memory(&module);
644
645        // Both tensors should be allocated, each 512 bytes.
646        let non_zero: Vec<_> = plan
647            .allocations
648            .iter()
649            .filter(|a| a.size_bytes > 0)
650            .collect();
651        assert_eq!(non_zero.len(), 2);
652
653        // Peak should be 512 (reuse), not 1024 (no reuse).
654        assert_eq!(plan.peak_bytes, 512);
655    }
656
657    #[test]
658    fn overlapping_lifetimes_no_reuse() {
659        // Two local tensors both used across overlapping statements.
660        let mut module = Module::default();
661        let arr_ty = f32_array_type(&mut module, 128); // 512 bytes each
662
663        let mut func = Function::new("main");
664
665        let lv_a = func.local_variables.append(LocalVariable {
666            name: Some("a".into()),
667            ty: arr_ty,
668            init: None,
669        });
670        let lv_b = func.local_variables.append(LocalVariable {
671            name: Some("b".into()),
672            ty: arr_ty,
673            init: None,
674        });
675
676        let ptr_a = func.expressions.append(Expression::LocalVariable(lv_a));
677        let ptr_b = func.expressions.append(Expression::LocalVariable(lv_b));
678        let lit = func
679            .expressions
680            .append(Expression::Literal(Literal::F32(0.0)));
681
682        // stmt 0: store to A (A is live)
683        func.body.push(Statement::Store {
684            pointer: ptr_a,
685            value: lit,
686        });
687        // stmt 1: read from A, store to B (both are live)
688        let load_a = func.expressions.append(Expression::Load { pointer: ptr_a });
689        func.body.push(Statement::Store {
690            pointer: ptr_b,
691            value: load_a,
692        });
693
694        module.entry_points.push(EntryPoint {
695            name: "main".into(),
696            workgroup_size: [1, 1, 1],
697            function: func,
698        });
699
700        let plan = plan_memory(&module);
701
702        // Both 512-byte tensors overlap (A: [0,1], B: [1,1]) so peak must be 1024.
703        let non_zero: Vec<_> = plan
704            .allocations
705            .iter()
706            .filter(|a| a.size_bytes > 0)
707            .collect();
708        assert_eq!(non_zero.len(), 2);
709        assert_eq!(plan.peak_bytes, 1024);
710    }
711
712    #[test]
713    fn peak_memory_less_than_sum_of_all_tensors() {
714        // Three tensors with staggered, non-overlapping lifetimes.
715        // Total = 3 * 512 = 1536, but peak should be 512 (full reuse).
716        let mut module = Module::default();
717        let arr_ty = f32_array_type(&mut module, 128); // 512 bytes each
718
719        let mut func = Function::new("main");
720
721        let lv_a = func.local_variables.append(LocalVariable {
722            name: Some("a".into()),
723            ty: arr_ty,
724            init: None,
725        });
726        let lv_b = func.local_variables.append(LocalVariable {
727            name: Some("b".into()),
728            ty: arr_ty,
729            init: None,
730        });
731        let lv_c = func.local_variables.append(LocalVariable {
732            name: Some("c".into()),
733            ty: arr_ty,
734            init: None,
735        });
736
737        let ptr_a = func.expressions.append(Expression::LocalVariable(lv_a));
738        let ptr_b = func.expressions.append(Expression::LocalVariable(lv_b));
739        let ptr_c = func.expressions.append(Expression::LocalVariable(lv_c));
740        let lit = func
741            .expressions
742            .append(Expression::Literal(Literal::F32(0.0)));
743
744        // Each tensor used in exactly one statement, no overlap.
745        func.body.push(Statement::Store {
746            pointer: ptr_a,
747            value: lit,
748        });
749        func.body.push(Statement::Store {
750            pointer: ptr_b,
751            value: lit,
752        });
753        func.body.push(Statement::Store {
754            pointer: ptr_c,
755            value: lit,
756        });
757
758        module.entry_points.push(EntryPoint {
759            name: "main".into(),
760            workgroup_size: [1, 1, 1],
761            function: func,
762        });
763
764        let plan = plan_memory(&module);
765
766        let total: usize = plan.allocations.iter().map(|a| a.size_bytes).sum();
767        assert_eq!(total, 1536); // 3 * 512
768        // Reuse should bring peak well below total.
769        assert!(
770            plan.peak_bytes < total,
771            "peak {} should be < total {}",
772            plan.peak_bytes,
773            total
774        );
775        assert_eq!(plan.peak_bytes, 512); // all three reuse the same slot
776    }
777
778    #[test]
779    fn dynamic_tensors_get_zero_size() {
780        let mut module = Module::default();
781        let dyn_ty = f32_dynamic_array_type(&mut module);
782
783        module.global_variables.append(GlobalVariable {
784            name: Some("dyn_buf".into()),
785            space: AddressSpace::Storage {
786                access: StorageAccess::LOAD,
787            },
788            binding: Some(ResourceBinding {
789                group: 0,
790                binding: 0,
791            }),
792            ty: dyn_ty,
793            init: None,
794            layout: None,
795        });
796
797        let plan = plan_memory(&module);
798        // Dynamic tensors have size 0, so they don't contribute to peak.
799        assert_eq!(plan.peak_bytes, 0);
800    }
801
802    #[test]
803    fn tensor_type_size_computation() {
804        let mut module = Module::default();
805
806        // Tensor<f32>[1, 224, 224, 3] => 1*224*224*3*4 = 602112 bytes
807        let tensor_ty = module.types.insert(Type {
808            name: None,
809            inner: TypeInner::Tensor {
810                scalar: Scalar::F32,
811                shape: TensorShape::fixed(&[1, 224, 224, 3]),
812            },
813        });
814
815        assert_eq!(type_size_bytes(&module, tensor_ty), 602_112);
816
817        // Dynamic tensor => 0
818        let dyn_tensor_ty = module.types.insert(Type {
819            name: None,
820            inner: TypeInner::Tensor {
821                scalar: Scalar::F32,
822                shape: TensorShape::all_dynamic(4),
823            },
824        });
825        assert_eq!(type_size_bytes(&module, dyn_tensor_ty), 0);
826    }
827
828    #[test]
829    fn memory_plan_display() {
830        let plan = MemoryPlan {
831            allocations: vec![
832                BufferAllocation {
833                    tensor_id: TensorId(0),
834                    offset: 0,
835                    size_bytes: 1024,
836                },
837                BufferAllocation {
838                    tensor_id: TensorId(1),
839                    offset: 0,
840                    size_bytes: 512,
841                },
842            ],
843            peak_bytes: 1024,
844        };
845
846        let text = format!("{plan}");
847        assert!(text.contains("Peak memory: 1024 bytes"));
848        assert!(text.contains("Buffers: 2"));
849        assert!(text.contains("Reuse savings:"));
850        assert!(text.contains("tensor_0"));
851        assert!(text.contains("tensor_1"));
852    }
853
854    #[test]
855    fn memory_planning_pass_runs() {
856        let mut module = Module::default();
857        let pass = MemoryPlanning;
858        assert_eq!(pass.name(), "MemoryPlanning");
859        let changed = pass.run(&mut module);
860        assert!(!changed); // analysis pass never modifies IR
861    }
862
863    #[test]
864    fn input_compute_output_pipeline() {
865        // Simulates: input -> compute -> output
866        // input and output don't overlap with the temporary.
867        let mut module = Module::default();
868        let arr_ty = f32_array_type(&mut module, 256); // 1024 bytes
869
870        let gv_input = module.global_variables.append(GlobalVariable {
871            name: Some("input".into()),
872            space: AddressSpace::Storage {
873                access: StorageAccess::LOAD,
874            },
875            binding: Some(ResourceBinding {
876                group: 0,
877                binding: 0,
878            }),
879            ty: arr_ty,
880            init: None,
881            layout: None,
882        });
883        let gv_output = module.global_variables.append(GlobalVariable {
884            name: Some("output".into()),
885            space: AddressSpace::Storage {
886                access: StorageAccess::STORE,
887            },
888            binding: Some(ResourceBinding {
889                group: 0,
890                binding: 1,
891            }),
892            ty: arr_ty,
893            init: None,
894            layout: None,
895        });
896
897        let mut func = Function::new("main");
898
899        let temp = func.local_variables.append(LocalVariable {
900            name: Some("temp".into()),
901            ty: arr_ty,
902            init: None,
903        });
904
905        let ptr_in = func
906            .expressions
907            .append(Expression::GlobalVariable(gv_input));
908        let ptr_out = func
909            .expressions
910            .append(Expression::GlobalVariable(gv_output));
911        let ptr_temp = func.expressions.append(Expression::LocalVariable(temp));
912        let load_in = func
913            .expressions
914            .append(Expression::Load { pointer: ptr_in });
915        let load_temp = func
916            .expressions
917            .append(Expression::Load { pointer: ptr_temp });
918
919        // stmt 0: temp = load(input) -- input and temp are live
920        func.body.push(Statement::Store {
921            pointer: ptr_temp,
922            value: load_in,
923        });
924        // stmt 1: output = load(temp) -- temp and output are live
925        func.body.push(Statement::Store {
926            pointer: ptr_out,
927            value: load_temp,
928        });
929
930        module.entry_points.push(EntryPoint {
931            name: "main".into(),
932            workgroup_size: [256, 1, 1],
933            function: func,
934        });
935
936        let plan = plan_memory(&module);
937
938        // input: [0,0], temp: [0,1], output: [1,1]
939        // input and output don't overlap, so they can share a slot.
940        // Peak should be 2048 (temp + one of input/output), not 3072.
941        assert!(
942            plan.peak_bytes <= 2048,
943            "peak {} should be <= 2048 (reuse between input and output)",
944            plan.peak_bytes
945        );
946        assert!(plan.peak_bytes > 0);
947    }
948
949    #[test]
950    fn many_temporaries_reuse() {
951        // 10 temporaries, each used in exactly one statement, no overlap.
952        let mut module = Module::default();
953        let arr_ty = f32_array_type(&mut module, 64); // 256 bytes each
954
955        let mut func = Function::new("main");
956        let lit = func
957            .expressions
958            .append(Expression::Literal(Literal::F32(0.0)));
959
960        for i in 0..10 {
961            let lv = func.local_variables.append(LocalVariable {
962                name: Some(format!("temp_{i}")),
963                ty: arr_ty,
964                init: None,
965            });
966            let ptr = func.expressions.append(Expression::LocalVariable(lv));
967            func.body.push(Statement::Store {
968                pointer: ptr,
969                value: lit,
970            });
971        }
972
973        module.entry_points.push(EntryPoint {
974            name: "main".into(),
975            workgroup_size: [1, 1, 1],
976            function: func,
977        });
978
979        let plan = plan_memory(&module);
980
981        let total: usize = plan.allocations.iter().map(|a| a.size_bytes).sum();
982        assert_eq!(total, 2560); // 10 * 256
983        // All should reuse the same 256-byte slot.
984        assert_eq!(plan.peak_bytes, 256);
985    }
986
987    #[test]
988    fn struct_type_size() {
989        let mut module = Module::default();
990        let struct_ty = module.types.insert(Type {
991            name: Some("Params".into()),
992            inner: TypeInner::Struct {
993                members: vec![],
994                span: 64,
995            },
996        });
997        assert_eq!(type_size_bytes(&module, struct_ty), 64);
998    }
999
1000    #[test]
1001    fn vector_and_matrix_type_sizes() {
1002        let mut module = Module::default();
1003
1004        let vec4_ty = module.types.insert(Type {
1005            name: None,
1006            inner: TypeInner::Vector {
1007                size: VectorSize::Quad,
1008                scalar: Scalar::F32,
1009            },
1010        });
1011        assert_eq!(type_size_bytes(&module, vec4_ty), 16); // 4 * 4
1012
1013        let mat4x4_ty = module.types.insert(Type {
1014            name: None,
1015            inner: TypeInner::Matrix {
1016                columns: VectorSize::Quad,
1017                rows: VectorSize::Quad,
1018                scalar: Scalar::F32,
1019            },
1020        });
1021        assert_eq!(type_size_bytes(&module, mat4x4_ty), 64); // 4 * 4 * 4
1022    }
1023
1024    // ===== Type size: Atomic =====
1025
1026    #[test]
1027    fn atomic_type_size() {
1028        let mut module = Module::default();
1029        let atomic_ty = module.types.insert(Type {
1030            name: None,
1031            inner: TypeInner::Atomic(Scalar::U32),
1032        });
1033        assert_eq!(type_size_bytes(&module, atomic_ty), 4);
1034    }
1035
1036    // ===== Type size: Pointer =====
1037
1038    #[test]
1039    fn pointer_type_size_is_zero() {
1040        let mut module = Module::default();
1041        let f32_ty = f32_type(&mut module);
1042        let ptr_ty = module.types.insert(Type {
1043            name: None,
1044            inner: TypeInner::Pointer {
1045                base: f32_ty,
1046                space: AddressSpace::Storage {
1047                    access: StorageAccess::LOAD,
1048                },
1049            },
1050        });
1051        assert_eq!(type_size_bytes(&module, ptr_ty), 0);
1052    }
1053
1054    // ===== Type size: Array with zero-size base uses stride =====
1055
1056    #[test]
1057    fn array_with_zero_base_uses_stride() {
1058        let mut module = Module::default();
1059        // Create an array whose base type has zero size (e.g. pointer).
1060        let f32_ty = f32_type(&mut module);
1061        let ptr_base = module.types.insert(Type {
1062            name: None,
1063            inner: TypeInner::Pointer {
1064                base: f32_ty,
1065                space: AddressSpace::Storage {
1066                    access: StorageAccess::LOAD,
1067                },
1068            },
1069        });
1070        let arr_ty = module.types.insert(Type {
1071            name: None,
1072            inner: TypeInner::Array {
1073                base: ptr_base,
1074                size: ArraySize::Constant(10),
1075                stride: 8,
1076            },
1077        });
1078        // Base has size 0, so falls back to n * stride = 10 * 8 = 80.
1079        assert_eq!(type_size_bytes(&module, arr_ty), 80);
1080    }
1081
1082    // ===== Type size: Array with non-zero base uses base size =====
1083
1084    #[test]
1085    fn array_with_nonzero_base_uses_elem_size() {
1086        let mut module = Module::default();
1087        let arr_ty = f32_array_type(&mut module, 10);
1088        // f32 = 4 bytes, so 10 * 4 = 40.
1089        assert_eq!(type_size_bytes(&module, arr_ty), 40);
1090    }
1091
1092    // ===== Type size: Dynamic array =====
1093
1094    #[test]
1095    fn dynamic_array_size_is_zero() {
1096        let mut module = Module::default();
1097        let dyn_arr_ty = f32_dynamic_array_type(&mut module);
1098        assert_eq!(type_size_bytes(&module, dyn_arr_ty), 0);
1099    }
1100
1101    // ===== Type size: Scalar =====
1102
1103    #[test]
1104    fn scalar_type_size() {
1105        let mut module = Module::default();
1106        let f32_ty = f32_type(&mut module);
1107        assert_eq!(type_size_bytes(&module, f32_ty), 4);
1108    }
1109
1110    // ===== Type size: Vec2 =====
1111
1112    #[test]
1113    fn vec2_type_size() {
1114        let mut module = Module::default();
1115        let vec2_ty = module.types.insert(Type {
1116            name: None,
1117            inner: TypeInner::Vector {
1118                size: VectorSize::Bi,
1119                scalar: Scalar::F32,
1120            },
1121        });
1122        assert_eq!(type_size_bytes(&module, vec2_ty), 8); // 2 * 4
1123    }
1124
1125    // ===== Type size: Vec3 =====
1126
1127    #[test]
1128    fn vec3_type_size() {
1129        let mut module = Module::default();
1130        let vec3_ty = module.types.insert(Type {
1131            name: None,
1132            inner: TypeInner::Vector {
1133                size: VectorSize::Tri,
1134                scalar: Scalar::F32,
1135            },
1136        });
1137        assert_eq!(type_size_bytes(&module, vec3_ty), 12); // 3 * 4
1138    }
1139
1140    // ===== Type size: Matrix 2x3 =====
1141
1142    #[test]
1143    fn matrix_2x3_type_size() {
1144        let mut module = Module::default();
1145        let mat_ty = module.types.insert(Type {
1146            name: None,
1147            inner: TypeInner::Matrix {
1148                columns: VectorSize::Bi,
1149                rows: VectorSize::Tri,
1150                scalar: Scalar::F32,
1151            },
1152        });
1153        assert_eq!(type_size_bytes(&module, mat_ty), 24); // 2 * 3 * 4
1154    }
1155
1156    // ===== Greedy allocate: empty intervals =====
1157
1158    #[test]
1159    fn greedy_allocate_empty() {
1160        let intervals = HashMap::new();
1161        let plan = greedy_allocate(&intervals);
1162        assert_eq!(plan.allocations.len(), 0);
1163        assert_eq!(plan.peak_bytes, 0);
1164    }
1165
1166    // ===== Greedy allocate: zero-size tensor =====
1167
1168    #[test]
1169    fn greedy_allocate_zero_size_tensor() {
1170        let mut intervals = HashMap::new();
1171        intervals.insert(
1172            TensorId(0),
1173            LiveInterval {
1174                start: 0,
1175                end: 5,
1176                size_bytes: 0,
1177            },
1178        );
1179        let plan = greedy_allocate(&intervals);
1180        assert_eq!(plan.allocations.len(), 1);
1181        assert_eq!(plan.allocations[0].size_bytes, 0);
1182        assert_eq!(plan.peak_bytes, 0);
1183    }
1184
1185    // ===== Greedy allocate: gap finding between active allocations =====
1186
1187    #[test]
1188    fn greedy_allocate_gap_finding() {
1189        // Three tensors: A and C overlap, B does not overlap with anything.
1190        // A: [0, 3] size 100, C: [0, 3] size 100, B: [0, 3] size 50.
1191        // All overlap, so they must be placed sequentially.
1192        // Then D: [5, 6] size 50 should fit in a gap (reuse B's slot).
1193        let mut intervals = HashMap::new();
1194        intervals.insert(
1195            TensorId(0),
1196            LiveInterval {
1197                start: 0,
1198                end: 3,
1199                size_bytes: 100,
1200            },
1201        );
1202        intervals.insert(
1203            TensorId(1),
1204            LiveInterval {
1205                start: 0,
1206                end: 3,
1207                size_bytes: 50,
1208            },
1209        );
1210        intervals.insert(
1211            TensorId(2),
1212            LiveInterval {
1213                start: 0,
1214                end: 3,
1215                size_bytes: 100,
1216            },
1217        );
1218        intervals.insert(
1219            TensorId(3),
1220            LiveInterval {
1221                start: 5,
1222                end: 6,
1223                size_bytes: 50,
1224            },
1225        );
1226
1227        let plan = greedy_allocate(&intervals);
1228        assert_eq!(plan.allocations.len(), 4);
1229        // Peak should be 250 (all three overlapping: 100 + 50 + 100).
1230        // D should reuse space since its lifetime doesn't overlap.
1231        assert_eq!(plan.peak_bytes, 250);
1232    }
1233
1234    // ===== Greedy allocate: size tie-breaking (larger first) =====
1235
1236    #[test]
1237    fn greedy_allocate_size_tiebreak() {
1238        // Two tensors starting at the same time, different sizes.
1239        let mut intervals = HashMap::new();
1240        intervals.insert(
1241            TensorId(0),
1242            LiveInterval {
1243                start: 0,
1244                end: 0,
1245                size_bytes: 50,
1246            },
1247        );
1248        intervals.insert(
1249            TensorId(1),
1250            LiveInterval {
1251                start: 0,
1252                end: 0,
1253                size_bytes: 200,
1254            },
1255        );
1256
1257        let plan = greedy_allocate(&intervals);
1258        assert_eq!(plan.allocations.len(), 2);
1259        // Both overlap at time 0, so peak should be 250.
1260        assert_eq!(plan.peak_bytes, 250);
1261
1262        // Larger tensor should be placed first (lower offset) due to tie-breaking.
1263        let large_alloc = plan
1264            .allocations
1265            .iter()
1266            .find(|a| a.size_bytes == 200)
1267            .unwrap();
1268        let small_alloc = plan
1269            .allocations
1270            .iter()
1271            .find(|a| a.size_bytes == 50)
1272            .unwrap();
1273        assert!(
1274            large_alloc.offset < small_alloc.offset,
1275            "larger tensor should have lower offset"
1276        );
1277    }
1278
1279    // ===== Merge intervals =====
1280
1281    #[test]
1282    fn merge_intervals_extends_existing() {
1283        let mut dst = HashMap::new();
1284        dst.insert(
1285            TensorId(0),
1286            LiveInterval {
1287                start: 2,
1288                end: 5,
1289                size_bytes: 100,
1290            },
1291        );
1292
1293        let mut src = HashMap::new();
1294        src.insert(
1295            TensorId(0),
1296            LiveInterval {
1297                start: 1,
1298                end: 3,
1299                size_bytes: 100,
1300            },
1301        );
1302        src.insert(
1303            TensorId(1),
1304            LiveInterval {
1305                start: 0,
1306                end: 4,
1307                size_bytes: 200,
1308            },
1309        );
1310
1311        merge_intervals(&mut dst, &src);
1312
1313        // TensorId(0) should have start extended to 1, end stays at 5.
1314        let interval_0 = dst.get(&TensorId(0)).unwrap();
1315        assert_eq!(interval_0.start, 1);
1316        assert_eq!(interval_0.end, 5);
1317
1318        // TensorId(1) should be added.
1319        let interval_1 = dst.get(&TensorId(1)).unwrap();
1320        assert_eq!(interval_1.start, 0);
1321        assert_eq!(interval_1.end, 4);
1322        assert_eq!(interval_1.size_bytes, 200);
1323    }
1324
1325    #[test]
1326    fn merge_intervals_end_extended() {
1327        let mut dst = HashMap::new();
1328        dst.insert(
1329            TensorId(0),
1330            LiveInterval {
1331                start: 0,
1332                end: 3,
1333                size_bytes: 100,
1334            },
1335        );
1336
1337        let mut src = HashMap::new();
1338        src.insert(
1339            TensorId(0),
1340            LiveInterval {
1341                start: 2,
1342                end: 10,
1343                size_bytes: 100,
1344            },
1345        );
1346
1347        merge_intervals(&mut dst, &src);
1348        let interval = dst.get(&TensorId(0)).unwrap();
1349        assert_eq!(interval.start, 0);
1350        assert_eq!(interval.end, 10);
1351    }
1352
1353    // ===== Plan memory with helper functions =====
1354
1355    #[test]
1356    fn plan_memory_with_helper_function() {
1357        let mut module = Module::default();
1358        let arr_ty = f32_array_type(&mut module, 64); // 256 bytes
1359
1360        let gv = module.global_variables.append(GlobalVariable {
1361            name: Some("buf".into()),
1362            space: AddressSpace::Storage {
1363                access: StorageAccess::LOAD | StorageAccess::STORE,
1364            },
1365            binding: Some(ResourceBinding {
1366                group: 0,
1367                binding: 0,
1368            }),
1369            ty: arr_ty,
1370            init: None,
1371            layout: None,
1372        });
1373
1374        // Helper function that uses the global.
1375        let mut helper = Function::new("helper");
1376        let ptr = helper.expressions.append(Expression::GlobalVariable(gv));
1377        let lit = helper
1378            .expressions
1379            .append(Expression::Literal(Literal::F32(0.0)));
1380        helper.body.push(Statement::Store {
1381            pointer: ptr,
1382            value: lit,
1383        });
1384        module.functions.append(helper);
1385
1386        // Entry point that also uses the global.
1387        let mut ep_func = Function::new("main");
1388        let ptr2 = ep_func.expressions.append(Expression::GlobalVariable(gv));
1389        let lit2 = ep_func
1390            .expressions
1391            .append(Expression::Literal(Literal::F32(1.0)));
1392        ep_func.body.push(Statement::Store {
1393            pointer: ptr2,
1394            value: lit2,
1395        });
1396        module.entry_points.push(EntryPoint {
1397            name: "main".into(),
1398            workgroup_size: [1, 1, 1],
1399            function: ep_func,
1400        });
1401
1402        let plan = plan_memory(&module);
1403        // Should have at least 1 allocation for the global.
1404        assert_ne!(plan.allocations.len(), 0);
1405        assert!(plan.peak_bytes >= 256);
1406    }
1407
1408    // ===== Plan memory: unreferenced globals get allocated =====
1409
1410    #[test]
1411    fn unreferenced_globals_get_allocated() {
1412        let mut module = Module::default();
1413        let arr_ty = f32_array_type(&mut module, 128); // 512 bytes
1414
1415        // Global variable that is never referenced in any function body.
1416        module.global_variables.append(GlobalVariable {
1417            name: Some("unused".into()),
1418            space: AddressSpace::Storage {
1419                access: StorageAccess::LOAD,
1420            },
1421            binding: Some(ResourceBinding {
1422                group: 0,
1423                binding: 0,
1424            }),
1425            ty: arr_ty,
1426            init: None,
1427            layout: None,
1428        });
1429
1430        let plan = plan_memory(&module);
1431        // The unreferenced global should still get an allocation.
1432        let non_zero: Vec<_> = plan
1433            .allocations
1434            .iter()
1435            .filter(|a| a.size_bytes > 0)
1436            .collect();
1437        assert_eq!(non_zero.len(), 1);
1438        assert_eq!(non_zero[0].size_bytes, 512);
1439        assert_eq!(plan.peak_bytes, 512);
1440    }
1441
1442    // ===== Lifetime analysis with If statement =====
1443
1444    #[test]
1445    fn lifetime_analysis_with_if_statement() {
1446        let mut module = Module::default();
1447        let arr_ty = f32_array_type(&mut module, 64); // 256 bytes
1448
1449        let gv = module.global_variables.append(GlobalVariable {
1450            name: Some("buf".into()),
1451            space: AddressSpace::Storage {
1452                access: StorageAccess::LOAD | StorageAccess::STORE,
1453            },
1454            binding: Some(ResourceBinding {
1455                group: 0,
1456                binding: 0,
1457            }),
1458            ty: arr_ty,
1459            init: None,
1460            layout: None,
1461        });
1462
1463        let mut func = Function::new("main");
1464        let ptr = func.expressions.append(Expression::GlobalVariable(gv));
1465        let cond = func
1466            .expressions
1467            .append(Expression::Literal(Literal::Bool(true)));
1468        let val = func
1469            .expressions
1470            .append(Expression::Literal(Literal::F32(1.0)));
1471
1472        // stmt 0: if (cond) { store val -> ptr }
1473        func.body.push(Statement::If {
1474            condition: cond,
1475            accept: vec![Statement::Store {
1476                pointer: ptr,
1477                value: val,
1478            }],
1479            reject: vec![],
1480        });
1481
1482        module.entry_points.push(EntryPoint {
1483            name: "main".into(),
1484            workgroup_size: [1, 1, 1],
1485            function: func,
1486        });
1487
1488        let plan = plan_memory(&module);
1489        assert!(plan.peak_bytes >= 256);
1490    }
1491
1492    // ===== Lifetime analysis with Loop statement =====
1493
1494    #[test]
1495    fn lifetime_analysis_with_loop_statement() {
1496        let mut module = Module::default();
1497        let arr_ty = f32_array_type(&mut module, 64); // 256 bytes
1498
1499        let gv = module.global_variables.append(GlobalVariable {
1500            name: Some("buf".into()),
1501            space: AddressSpace::Storage {
1502                access: StorageAccess::LOAD | StorageAccess::STORE,
1503            },
1504            binding: Some(ResourceBinding {
1505                group: 0,
1506                binding: 0,
1507            }),
1508            ty: arr_ty,
1509            init: None,
1510            layout: None,
1511        });
1512
1513        let mut func = Function::new("main");
1514        let ptr = func.expressions.append(Expression::GlobalVariable(gv));
1515        let val = func
1516            .expressions
1517            .append(Expression::Literal(Literal::F32(1.0)));
1518        let break_cond = func
1519            .expressions
1520            .append(Expression::Literal(Literal::Bool(false)));
1521
1522        // Loop with body, continuing, and break_if.
1523        func.body.push(Statement::Loop {
1524            body: vec![Statement::Store {
1525                pointer: ptr,
1526                value: val,
1527            }],
1528            continuing: vec![Statement::Store {
1529                pointer: ptr,
1530                value: val,
1531            }],
1532            break_if: Some(break_cond),
1533        });
1534
1535        module.entry_points.push(EntryPoint {
1536            name: "main".into(),
1537            workgroup_size: [1, 1, 1],
1538            function: func,
1539        });
1540
1541        let plan = plan_memory(&module);
1542        assert!(plan.peak_bytes >= 256);
1543    }
1544
1545    // ===== Lifetime analysis with Call statement =====
1546
1547    #[test]
1548    fn lifetime_analysis_with_call_statement() {
1549        let mut module = Module::default();
1550        let arr_ty = f32_array_type(&mut module, 64); // 256 bytes
1551
1552        let gv = module.global_variables.append(GlobalVariable {
1553            name: Some("buf".into()),
1554            space: AddressSpace::Storage {
1555                access: StorageAccess::LOAD | StorageAccess::STORE,
1556            },
1557            binding: Some(ResourceBinding {
1558                group: 0,
1559                binding: 0,
1560            }),
1561            ty: arr_ty,
1562            init: None,
1563            layout: None,
1564        });
1565
1566        // Add a helper function.
1567        let helper = Function::new("helper");
1568        let helper_handle = module.functions.append(helper);
1569
1570        let mut func = Function::new("main");
1571        let ptr = func.expressions.append(Expression::GlobalVariable(gv));
1572        let result_expr = func
1573            .expressions
1574            .append(Expression::Literal(Literal::F32(0.0)));
1575
1576        func.body.push(Statement::Call {
1577            function: helper_handle,
1578            arguments: vec![ptr],
1579            result: Some(result_expr),
1580        });
1581
1582        module.entry_points.push(EntryPoint {
1583            name: "main".into(),
1584            workgroup_size: [1, 1, 1],
1585            function: func,
1586        });
1587
1588        let plan = plan_memory(&module);
1589        assert!(plan.peak_bytes >= 256);
1590    }
1591
1592    // ===== Lifetime analysis with Atomic statement =====
1593
1594    #[test]
1595    fn lifetime_analysis_with_atomic_statement() {
1596        let mut module = Module::default();
1597        let u32_ty = module.types.insert(Type {
1598            name: None,
1599            inner: TypeInner::Scalar(Scalar::U32),
1600        });
1601
1602        let gv = module.global_variables.append(GlobalVariable {
1603            name: Some("counter".into()),
1604            space: AddressSpace::Storage {
1605                access: StorageAccess::LOAD | StorageAccess::STORE,
1606            },
1607            binding: Some(ResourceBinding {
1608                group: 0,
1609                binding: 0,
1610            }),
1611            ty: u32_ty,
1612            init: None,
1613            layout: None,
1614        });
1615
1616        let mut func = Function::new("main");
1617        let ptr = func.expressions.append(Expression::GlobalVariable(gv));
1618        let val = func
1619            .expressions
1620            .append(Expression::Literal(Literal::U32(1)));
1621        let result_expr = func
1622            .expressions
1623            .append(Expression::Literal(Literal::U32(0)));
1624
1625        func.body.push(Statement::Atomic {
1626            pointer: ptr,
1627            fun: AtomicFunction::Add,
1628            value: val,
1629            result: Some(result_expr),
1630        });
1631
1632        module.entry_points.push(EntryPoint {
1633            name: "main".into(),
1634            workgroup_size: [1, 1, 1],
1635            function: func,
1636        });
1637
1638        let plan = plan_memory(&module);
1639        // u32 = 4 bytes.
1640        assert!(plan.peak_bytes >= 4);
1641    }
1642
1643    // ===== Lifetime analysis with Atomic Exchange compare =====
1644
1645    #[test]
1646    fn lifetime_analysis_with_atomic_exchange_compare() {
1647        let mut module = Module::default();
1648        let u32_ty = module.types.insert(Type {
1649            name: None,
1650            inner: TypeInner::Scalar(Scalar::U32),
1651        });
1652
1653        let gv = module.global_variables.append(GlobalVariable {
1654            name: Some("counter".into()),
1655            space: AddressSpace::Storage {
1656                access: StorageAccess::LOAD | StorageAccess::STORE,
1657            },
1658            binding: Some(ResourceBinding {
1659                group: 0,
1660                binding: 0,
1661            }),
1662            ty: u32_ty,
1663            init: None,
1664            layout: None,
1665        });
1666
1667        let mut func = Function::new("main");
1668        let ptr = func.expressions.append(Expression::GlobalVariable(gv));
1669        let val = func
1670            .expressions
1671            .append(Expression::Literal(Literal::U32(1)));
1672        let cmp = func
1673            .expressions
1674            .append(Expression::Literal(Literal::U32(0)));
1675
1676        func.body.push(Statement::Atomic {
1677            pointer: ptr,
1678            fun: AtomicFunction::Exchange { compare: Some(cmp) },
1679            value: val,
1680            result: None,
1681        });
1682
1683        module.entry_points.push(EntryPoint {
1684            name: "main".into(),
1685            workgroup_size: [1, 1, 1],
1686            function: func,
1687        });
1688
1689        let plan = plan_memory(&module);
1690        assert!(plan.peak_bytes >= 4);
1691    }
1692
1693    // ===== Lifetime analysis with Return statement =====
1694
1695    #[test]
1696    fn lifetime_analysis_with_return_value() {
1697        let mut module = Module::default();
1698        let arr_ty = f32_array_type(&mut module, 64); // 256 bytes
1699
1700        let gv = module.global_variables.append(GlobalVariable {
1701            name: Some("buf".into()),
1702            space: AddressSpace::Storage {
1703                access: StorageAccess::LOAD,
1704            },
1705            binding: Some(ResourceBinding {
1706                group: 0,
1707                binding: 0,
1708            }),
1709            ty: arr_ty,
1710            init: None,
1711            layout: None,
1712        });
1713
1714        let mut func = Function::new("main");
1715        let ptr = func.expressions.append(Expression::GlobalVariable(gv));
1716        func.body.push(Statement::Return { value: Some(ptr) });
1717
1718        module.entry_points.push(EntryPoint {
1719            name: "main".into(),
1720            workgroup_size: [1, 1, 1],
1721            function: func,
1722        });
1723
1724        let plan = plan_memory(&module);
1725        assert!(plan.peak_bytes >= 256);
1726    }
1727
1728    // ===== Lifetime analysis with Break/Continue (no tensor refs) =====
1729
1730    #[test]
1731    fn lifetime_analysis_break_continue_no_effect() {
1732        let mut module = Module::default();
1733        let mut func = Function::new("main");
1734        func.body.push(Statement::Break);
1735        func.body.push(Statement::Continue);
1736
1737        module.entry_points.push(EntryPoint {
1738            name: "main".into(),
1739            workgroup_size: [1, 1, 1],
1740            function: func,
1741        });
1742
1743        let plan = plan_memory(&module);
1744        assert_eq!(plan.allocations.len(), 0);
1745        assert_eq!(plan.peak_bytes, 0);
1746    }
1747
1748    // ===== Lifetime analysis with Barrier (no tensor refs) =====
1749
1750    #[test]
1751    fn lifetime_analysis_barrier_no_effect() {
1752        let mut module = Module::default();
1753        let mut func = Function::new("main");
1754        func.body.push(Statement::Barrier(Barrier::STORAGE));
1755
1756        module.entry_points.push(EntryPoint {
1757            name: "main".into(),
1758            workgroup_size: [1, 1, 1],
1759            function: func,
1760        });
1761
1762        let plan = plan_memory(&module);
1763        assert_eq!(plan.allocations.len(), 0);
1764        assert_eq!(plan.peak_bytes, 0);
1765    }
1766
1767    // ===== Lifetime analysis with Emit =====
1768
1769    #[test]
1770    fn lifetime_analysis_with_emit() {
1771        let mut module = Module::default();
1772        let arr_ty = f32_array_type(&mut module, 64); // 256 bytes
1773
1774        let gv = module.global_variables.append(GlobalVariable {
1775            name: Some("buf".into()),
1776            space: AddressSpace::Storage {
1777                access: StorageAccess::LOAD,
1778            },
1779            binding: Some(ResourceBinding {
1780                group: 0,
1781                binding: 0,
1782            }),
1783            ty: arr_ty,
1784            init: None,
1785            layout: None,
1786        });
1787
1788        let mut func = Function::new("main");
1789        let ptr = func.expressions.append(Expression::GlobalVariable(gv));
1790        let _load = func.expressions.append(Expression::Load { pointer: ptr });
1791
1792        // Emit covering both expressions.
1793        let range = Range::from_index_range(0..func.expressions.len() as u32);
1794        func.body.push(Statement::Emit(range));
1795
1796        module.entry_points.push(EntryPoint {
1797            name: "main".into(),
1798            workgroup_size: [1, 1, 1],
1799            function: func,
1800        });
1801
1802        let plan = plan_memory(&module);
1803        assert!(plan.peak_bytes >= 256);
1804    }
1805
1806    // ===== Access/AccessIndex expression resolution =====
1807
1808    #[test]
1809    fn access_index_resolves_to_global() {
1810        let mut module = Module::default();
1811        let arr_ty = f32_array_type(&mut module, 64); // 256 bytes
1812
1813        let gv = module.global_variables.append(GlobalVariable {
1814            name: Some("buf".into()),
1815            space: AddressSpace::Storage {
1816                access: StorageAccess::LOAD | StorageAccess::STORE,
1817            },
1818            binding: Some(ResourceBinding {
1819                group: 0,
1820                binding: 0,
1821            }),
1822            ty: arr_ty,
1823            init: None,
1824            layout: None,
1825        });
1826
1827        let mut func = Function::new("main");
1828        let ptr = func.expressions.append(Expression::GlobalVariable(gv));
1829        let access_idx = func.expressions.append(Expression::AccessIndex {
1830            base: ptr,
1831            index: 0,
1832        });
1833        let val = func
1834            .expressions
1835            .append(Expression::Literal(Literal::F32(1.0)));
1836
1837        // Store using AccessIndex (should resolve to the global).
1838        func.body.push(Statement::Store {
1839            pointer: access_idx,
1840            value: val,
1841        });
1842
1843        module.entry_points.push(EntryPoint {
1844            name: "main".into(),
1845            workgroup_size: [1, 1, 1],
1846            function: func,
1847        });
1848
1849        let plan = plan_memory(&module);
1850        assert!(plan.peak_bytes >= 256);
1851    }
1852
1853    // ===== Access expression resolution =====
1854
1855    #[test]
1856    fn access_resolves_to_global() {
1857        let mut module = Module::default();
1858        let arr_ty = f32_array_type(&mut module, 64); // 256 bytes
1859
1860        let gv = module.global_variables.append(GlobalVariable {
1861            name: Some("buf".into()),
1862            space: AddressSpace::Storage {
1863                access: StorageAccess::LOAD | StorageAccess::STORE,
1864            },
1865            binding: Some(ResourceBinding {
1866                group: 0,
1867                binding: 0,
1868            }),
1869            ty: arr_ty,
1870            init: None,
1871            layout: None,
1872        });
1873
1874        let mut func = Function::new("main");
1875        let ptr = func.expressions.append(Expression::GlobalVariable(gv));
1876        let idx = func
1877            .expressions
1878            .append(Expression::Literal(Literal::U32(0)));
1879        let access = func.expressions.append(Expression::Access {
1880            base: ptr,
1881            index: idx,
1882        });
1883        let val = func
1884            .expressions
1885            .append(Expression::Literal(Literal::F32(1.0)));
1886
1887        // Store using Access (should resolve to the global).
1888        func.body.push(Statement::Store {
1889            pointer: access,
1890            value: val,
1891        });
1892
1893        module.entry_points.push(EntryPoint {
1894            name: "main".into(),
1895            workgroup_size: [1, 1, 1],
1896            function: func,
1897        });
1898
1899        let plan = plan_memory(&module);
1900        assert!(plan.peak_bytes >= 256);
1901    }
1902
1903    // ===== Local variable in helper function =====
1904
1905    #[test]
1906    fn local_variable_in_helper_function() {
1907        let mut module = Module::default();
1908        let arr_ty = f32_array_type(&mut module, 64); // 256 bytes
1909
1910        let mut helper = Function::new("helper");
1911        let lv = helper.local_variables.append(LocalVariable {
1912            name: Some("temp".into()),
1913            ty: arr_ty,
1914            init: None,
1915        });
1916        let ptr = helper.expressions.append(Expression::LocalVariable(lv));
1917        let lit = helper
1918            .expressions
1919            .append(Expression::Literal(Literal::F32(0.0)));
1920        helper.body.push(Statement::Store {
1921            pointer: ptr,
1922            value: lit,
1923        });
1924        module.functions.append(helper);
1925
1926        let plan = plan_memory(&module);
1927        // The local variable should be allocated.
1928        let non_zero: Vec<_> = plan
1929            .allocations
1930            .iter()
1931            .filter(|a| a.size_bytes > 0)
1932            .collect();
1933        assert_eq!(non_zero.len(), 1);
1934        assert_eq!(non_zero[0].size_bytes, 256);
1935    }
1936
1937    // ===== Multiple entry points =====
1938
1939    #[test]
1940    fn plan_memory_multiple_entry_points() {
1941        let mut module = Module::default();
1942        let arr_ty = f32_array_type(&mut module, 128); // 512 bytes
1943
1944        let gv = module.global_variables.append(GlobalVariable {
1945            name: Some("shared".into()),
1946            space: AddressSpace::Storage {
1947                access: StorageAccess::LOAD | StorageAccess::STORE,
1948            },
1949            binding: Some(ResourceBinding {
1950                group: 0,
1951                binding: 0,
1952            }),
1953            ty: arr_ty,
1954            init: None,
1955            layout: None,
1956        });
1957
1958        // Entry point 1.
1959        let mut func1 = Function::new("ep1");
1960        let ptr1 = func1.expressions.append(Expression::GlobalVariable(gv));
1961        let lit1 = func1
1962            .expressions
1963            .append(Expression::Literal(Literal::F32(1.0)));
1964        func1.body.push(Statement::Store {
1965            pointer: ptr1,
1966            value: lit1,
1967        });
1968        module.entry_points.push(EntryPoint {
1969            name: "ep1".into(),
1970            workgroup_size: [1, 1, 1],
1971            function: func1,
1972        });
1973
1974        // Entry point 2.
1975        let mut func2 = Function::new("ep2");
1976        let ptr2 = func2.expressions.append(Expression::GlobalVariable(gv));
1977        let lit2 = func2
1978            .expressions
1979            .append(Expression::Literal(Literal::F32(2.0)));
1980        func2.body.push(Statement::Store {
1981            pointer: ptr2,
1982            value: lit2,
1983        });
1984        module.entry_points.push(EntryPoint {
1985            name: "ep2".into(),
1986            workgroup_size: [1, 1, 1],
1987            function: func2,
1988        });
1989
1990        let plan = plan_memory(&module);
1991        // Same global used in both entry points.
1992        assert_eq!(plan.peak_bytes, 512);
1993    }
1994
1995    // ===== MemoryPlanning pass with non-trivial module =====
1996
1997    #[test]
1998    fn memory_planning_pass_with_content() {
1999        let mut module = Module::default();
2000        let arr_ty = f32_array_type(&mut module, 128); // 512 bytes
2001
2002        let gv = module.global_variables.append(GlobalVariable {
2003            name: Some("buf".into()),
2004            space: AddressSpace::Storage {
2005                access: StorageAccess::LOAD | StorageAccess::STORE,
2006            },
2007            binding: Some(ResourceBinding {
2008                group: 0,
2009                binding: 0,
2010            }),
2011            ty: arr_ty,
2012            init: None,
2013            layout: None,
2014        });
2015
2016        let mut func = Function::new("main");
2017        let ptr = func.expressions.append(Expression::GlobalVariable(gv));
2018        let lit = func
2019            .expressions
2020            .append(Expression::Literal(Literal::F32(0.0)));
2021        func.body.push(Statement::Store {
2022            pointer: ptr,
2023            value: lit,
2024        });
2025        module.entry_points.push(EntryPoint {
2026            name: "main".into(),
2027            workgroup_size: [1, 1, 1],
2028            function: func,
2029        });
2030
2031        let pass = MemoryPlanning;
2032        let changed = pass.run(&mut module);
2033        assert!(!changed);
2034    }
2035
2036    // ===== Display: empty plan =====
2037
2038    #[test]
2039    fn memory_plan_display_empty() {
2040        let plan = MemoryPlan::default();
2041        let text = format!("{plan}");
2042        assert!(text.contains("Peak memory: 0 bytes"));
2043        assert!(text.contains("Buffers: 0"));
2044    }
2045
2046    // ===== Display: plan with zero-total (no reuse line) =====
2047
2048    #[test]
2049    fn memory_plan_display_no_reuse_line_for_zero_total() {
2050        let plan = MemoryPlan {
2051            allocations: vec![BufferAllocation {
2052                tensor_id: TensorId(0),
2053                offset: 0,
2054                size_bytes: 0,
2055            }],
2056            peak_bytes: 0,
2057        };
2058        let text = format!("{plan}");
2059        assert!(text.contains("Buffers: 1"));
2060        // With total = 0, the reuse savings line should not be present.
2061        assert!(!text.contains("Reuse savings:"));
2062    }
2063
2064    // ===== If statement with nested references in reject =====
2065
2066    #[test]
2067    fn if_with_reject_branch_references() {
2068        let mut module = Module::default();
2069        let arr_ty = f32_array_type(&mut module, 64); // 256 bytes
2070
2071        let gv_a = module.global_variables.append(GlobalVariable {
2072            name: Some("a".into()),
2073            space: AddressSpace::Storage {
2074                access: StorageAccess::LOAD | StorageAccess::STORE,
2075            },
2076            binding: Some(ResourceBinding {
2077                group: 0,
2078                binding: 0,
2079            }),
2080            ty: arr_ty,
2081            init: None,
2082            layout: None,
2083        });
2084        let gv_b = module.global_variables.append(GlobalVariable {
2085            name: Some("b".into()),
2086            space: AddressSpace::Storage {
2087                access: StorageAccess::LOAD | StorageAccess::STORE,
2088            },
2089            binding: Some(ResourceBinding {
2090                group: 0,
2091                binding: 1,
2092            }),
2093            ty: arr_ty,
2094            init: None,
2095            layout: None,
2096        });
2097
2098        let mut func = Function::new("main");
2099        let ptr_a = func.expressions.append(Expression::GlobalVariable(gv_a));
2100        let ptr_b = func.expressions.append(Expression::GlobalVariable(gv_b));
2101        let cond = func
2102            .expressions
2103            .append(Expression::Literal(Literal::Bool(true)));
2104        let val = func
2105            .expressions
2106            .append(Expression::Literal(Literal::F32(1.0)));
2107
2108        func.body.push(Statement::If {
2109            condition: cond,
2110            accept: vec![Statement::Store {
2111                pointer: ptr_a,
2112                value: val,
2113            }],
2114            reject: vec![Statement::Store {
2115                pointer: ptr_b,
2116                value: val,
2117            }],
2118        });
2119
2120        module.entry_points.push(EntryPoint {
2121            name: "main".into(),
2122            workgroup_size: [1, 1, 1],
2123            function: func,
2124        });
2125
2126        let plan = plan_memory(&module);
2127        // Both globals should be allocated.
2128        let non_zero: Vec<_> = plan
2129            .allocations
2130            .iter()
2131            .filter(|a| a.size_bytes > 0)
2132            .collect();
2133        assert_eq!(non_zero.len(), 2);
2134    }
2135
2136    // ===== Loop with break_if expression referencing a global =====
2137
2138    #[test]
2139    fn loop_break_if_references_global() {
2140        let mut module = Module::default();
2141        let u32_ty = module.types.insert(Type {
2142            name: None,
2143            inner: TypeInner::Scalar(Scalar::U32),
2144        });
2145
2146        let gv = module.global_variables.append(GlobalVariable {
2147            name: Some("flag".into()),
2148            space: AddressSpace::Storage {
2149                access: StorageAccess::LOAD,
2150            },
2151            binding: Some(ResourceBinding {
2152                group: 0,
2153                binding: 0,
2154            }),
2155            ty: u32_ty,
2156            init: None,
2157            layout: None,
2158        });
2159
2160        let mut func = Function::new("main");
2161        let ptr = func.expressions.append(Expression::GlobalVariable(gv));
2162
2163        func.body.push(Statement::Loop {
2164            body: vec![],
2165            continuing: vec![],
2166            break_if: Some(ptr),
2167        });
2168
2169        module.entry_points.push(EntryPoint {
2170            name: "main".into(),
2171            workgroup_size: [1, 1, 1],
2172            function: func,
2173        });
2174
2175        let plan = plan_memory(&module);
2176        assert!(plan.peak_bytes >= 4);
2177    }
2178
2179    // ===== Resolve expression: returns None for non-variable expressions =====
2180
2181    #[test]
2182    fn resolve_expr_non_variable_returns_none() {
2183        let mut module = Module::default();
2184        let mut func = Function::new("main");
2185        let lit = func
2186            .expressions
2187            .append(Expression::Literal(Literal::F32(1.0)));
2188
2189        // Store lit -> lit (not realistic, but tests resolve_expr_tensor_id returning None).
2190        func.body.push(Statement::Store {
2191            pointer: lit,
2192            value: lit,
2193        });
2194
2195        module.entry_points.push(EntryPoint {
2196            name: "main".into(),
2197            workgroup_size: [1, 1, 1],
2198            function: func,
2199        });
2200
2201        let plan = plan_memory(&module);
2202        // Literal expression does not resolve to any tensor, so no allocations.
2203        assert_eq!(plan.peak_bytes, 0);
2204    }
2205
2206    // ===== Return with no value in lifetime analysis =====
2207
2208    #[test]
2209    fn return_no_value_no_effect_on_lifetimes() {
2210        let mut module = Module::default();
2211        let mut func = Function::new("main");
2212        func.body.push(Statement::Return { value: None });
2213
2214        module.entry_points.push(EntryPoint {
2215            name: "main".into(),
2216            workgroup_size: [1, 1, 1],
2217            function: func,
2218        });
2219
2220        let plan = plan_memory(&module);
2221        assert_eq!(plan.peak_bytes, 0);
2222    }
2223
2224    // ===== Single store function (minimum case) =====
2225
2226    #[test]
2227    fn single_store_function() {
2228        let mut module = Module::default();
2229        let f32_ty = f32_type(&mut module);
2230
2231        let gv = module.global_variables.append(GlobalVariable {
2232            name: Some("x".into()),
2233            space: AddressSpace::Storage {
2234                access: StorageAccess::STORE,
2235            },
2236            binding: Some(ResourceBinding {
2237                group: 0,
2238                binding: 0,
2239            }),
2240            ty: f32_ty,
2241            init: None,
2242            layout: None,
2243        });
2244
2245        let mut func = Function::new("main");
2246        let ptr = func.expressions.append(Expression::GlobalVariable(gv));
2247        let val = func
2248            .expressions
2249            .append(Expression::Literal(Literal::F32(42.0)));
2250        func.body.push(Statement::Store {
2251            pointer: ptr,
2252            value: val,
2253        });
2254
2255        module.entry_points.push(EntryPoint {
2256            name: "main".into(),
2257            workgroup_size: [1, 1, 1],
2258            function: func,
2259        });
2260
2261        let plan = plan_memory(&module);
2262        assert_eq!(plan.peak_bytes, 4); // f32 = 4 bytes
2263    }
2264
2265    // ===== Call with no result in lifetime analysis =====
2266
2267    #[test]
2268    fn call_no_result_lifetime() {
2269        let mut module = Module::default();
2270        let arr_ty = f32_array_type(&mut module, 64); // 256 bytes
2271
2272        let gv = module.global_variables.append(GlobalVariable {
2273            name: Some("buf".into()),
2274            space: AddressSpace::Storage {
2275                access: StorageAccess::LOAD,
2276            },
2277            binding: Some(ResourceBinding {
2278                group: 0,
2279                binding: 0,
2280            }),
2281            ty: arr_ty,
2282            init: None,
2283            layout: None,
2284        });
2285
2286        let helper = Function::new("helper");
2287        let helper_handle = module.functions.append(helper);
2288
2289        let mut func = Function::new("main");
2290        let ptr = func.expressions.append(Expression::GlobalVariable(gv));
2291
2292        func.body.push(Statement::Call {
2293            function: helper_handle,
2294            arguments: vec![ptr],
2295            result: None,
2296        });
2297
2298        module.entry_points.push(EntryPoint {
2299            name: "main".into(),
2300            workgroup_size: [1, 1, 1],
2301            function: func,
2302        });
2303
2304        let plan = plan_memory(&module);
2305        assert!(plan.peak_bytes >= 256);
2306    }
2307
2308    // ===== Lifetime interval start updated backwards =====
2309
2310    #[test]
2311    fn lifetime_interval_start_updated_backwards() {
2312        // Build a case where a tensor is first seen at stmt 2, then at stmt 0.
2313        // This tests the and_modify path where stmt_idx < interval.start.
2314        let mut module = Module::default();
2315        let arr_ty = f32_array_type(&mut module, 64); // 256 bytes
2316
2317        let gv_a = module.global_variables.append(GlobalVariable {
2318            name: Some("a".into()),
2319            space: AddressSpace::Storage {
2320                access: StorageAccess::LOAD | StorageAccess::STORE,
2321            },
2322            binding: Some(ResourceBinding {
2323                group: 0,
2324                binding: 0,
2325            }),
2326            ty: arr_ty,
2327            init: None,
2328            layout: None,
2329        });
2330        let gv_b = module.global_variables.append(GlobalVariable {
2331            name: Some("b".into()),
2332            space: AddressSpace::Storage {
2333                access: StorageAccess::LOAD | StorageAccess::STORE,
2334            },
2335            binding: Some(ResourceBinding {
2336                group: 0,
2337                binding: 1,
2338            }),
2339            ty: arr_ty,
2340            init: None,
2341            layout: None,
2342        });
2343
2344        let mut func = Function::new("main");
2345        let ptr_a = func.expressions.append(Expression::GlobalVariable(gv_a));
2346        let ptr_b = func.expressions.append(Expression::GlobalVariable(gv_b));
2347        let load_a = func.expressions.append(Expression::Load { pointer: ptr_a });
2348        let val = func
2349            .expressions
2350            .append(Expression::Literal(Literal::F32(1.0)));
2351
2352        // stmt 0: store val -> ptr_a (a used at 0)
2353        func.body.push(Statement::Store {
2354            pointer: ptr_a,
2355            value: val,
2356        });
2357        // stmt 1: store val -> ptr_b (b used at 1)
2358        func.body.push(Statement::Store {
2359            pointer: ptr_b,
2360            value: val,
2361        });
2362        // stmt 2: store load(a) -> ptr_b (a used at 2, b used at 2)
2363        func.body.push(Statement::Store {
2364            pointer: ptr_b,
2365            value: load_a,
2366        });
2367
2368        module.entry_points.push(EntryPoint {
2369            name: "main".into(),
2370            workgroup_size: [1, 1, 1],
2371            function: func,
2372        });
2373
2374        let plan = plan_memory(&module);
2375        // a: [0, 2], b: [1, 2] -- both live at stmt 2, peak should be 512.
2376        assert_eq!(plan.peak_bytes, 512);
2377    }
2378}