Skip to main content

libsurfer/
frame_buffer.rs

1use ecolor::Color32;
2use egui::{CornerRadius, DragValue, Pos2, Rect, Sense, Stroke};
3use serde::{Deserialize, Serialize};
4use surfer_translation_types::VariableValue;
5
6use crate::wave_container::{ScopeRef, ScopeRefExt, VariableRef, VariableRefExt, WaveContainer};
7use crate::{Message, system_state::SystemState};
8
9#[derive(Serialize, Deserialize, Debug, Clone)]
10pub(crate) struct FrameBufferSettings {
11    pub pixels_per_row: usize,
12    pub square_pixels: bool,
13    #[serde(flatten)]
14    pub color_settings: PixelColorSettings,
15}
16
17#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
18pub(crate) struct PixelColorSettings {
19    pub rgb_mode: bool,
20    pub grayscale_bits: u8,
21    pub r_bits: u8,
22    pub g_bits: u8,
23    pub b_bits: u8,
24}
25
26impl Default for PixelColorSettings {
27    fn default() -> Self {
28        Self {
29            rgb_mode: false,
30            grayscale_bits: 1,
31            r_bits: 3,
32            g_bits: 3,
33            b_bits: 2,
34        }
35    }
36}
37
38impl Default for FrameBufferSettings {
39    fn default() -> Self {
40        Self {
41            pixels_per_row: 16,
42            square_pixels: true,
43            color_settings: PixelColorSettings::default(),
44        }
45    }
46}
47
48#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
49pub(crate) struct ArrayLevel {
50    pub min_index: i64,
51    pub max_index: i64,
52    pub first_index: i64,
53    pub last_index: i64,
54}
55
56#[derive(Debug, Clone, PartialEq, Eq)]
57pub(crate) struct FrameBufferContentCacheKey {
58    pub content: FrameBufferContent,
59    pub cursor_position: num::BigUint,
60}
61
62#[derive(Debug, Clone)]
63pub(crate) struct FrameBufferArrayCache {
64    pub key: FrameBufferContentCacheKey,
65    pub cached_value: Option<(String, u32)>,
66}
67
68#[derive(Debug, Clone, PartialEq, Eq)]
69pub(crate) struct FrameBufferPixelCacheKey {
70    pub array_key: FrameBufferContentCacheKey,
71    pub settings: PixelColorSettings,
72}
73
74#[derive(Debug, Clone)]
75pub(crate) struct FrameBufferPixelCache {
76    pub key: FrameBufferPixelCacheKey,
77    pub pixel_colors: Vec<Color32>,
78}
79
80#[derive(Debug, Clone, PartialEq, Eq)]
81pub(crate) enum FrameBufferContent {
82    Array {
83        scope_ref: ScopeRef,
84        /// One range-selector per level of array nesting.
85        /// The last level always applies to variables.
86        levels: Vec<ArrayLevel>,
87    },
88    Variable(VariableRef),
89}
90
91impl SystemState {
92    pub fn draw_frame_buffer_window(&mut self, ctx: &egui::Context, msgs: &mut Vec<Message>) {
93        let mut open = true;
94        egui::Window::new("Frame Buffer")
95            .open(&mut open)
96            .resizable(true)
97            .show(ctx, |ui| {
98                let frame_buffer_value = self.selected_variable_for_frame_buffer();
99                let Some((value, word_length, variable_name)) = frame_buffer_value.as_ref() else {
100                    ui.label("Place the cursor.");
101                    return;
102                };
103
104                let color_settings_key = {
105                    let settings = &mut self.user.frame_buffer;
106                    let color_settings = &mut settings.color_settings;
107
108                    ui.checkbox(&mut settings.square_pixels, "Square pixels");
109                    ui.checkbox(&mut color_settings.rgb_mode, "RGB mode");
110
111                    if color_settings.rgb_mode {
112                        ui.horizontal(|ui| {
113                            ui.label("R bits");
114                            ui.add(DragValue::new(&mut color_settings.r_bits).range(0..=8));
115                            ui.label("G bits");
116                            ui.add(DragValue::new(&mut color_settings.g_bits).range(0..=8));
117                            ui.label("B bits");
118                            ui.add(DragValue::new(&mut color_settings.b_bits).range(0..=8));
119                        });
120                    } else {
121                        ui.horizontal(|ui| {
122                            ui.label("Grayscale bits");
123                            ui.add(DragValue::new(&mut color_settings.grayscale_bits).range(1..=8));
124                        });
125                    }
126
127                    color_settings.clone()
128                };
129
130                ui.separator();
131
132                let bits = frame_buffer_bits(value, *word_length as usize);
133                if bits.is_empty() {
134                    ui.label("No bits available");
135                    return;
136                }
137
138                let Some(pixel_cache_key) =
139                    self.current_frame_buffer_array_cache_key()
140                        .map(|array_key| FrameBufferPixelCacheKey {
141                            array_key,
142                            settings: color_settings_key.clone(),
143                        })
144                else {
145                    ui.label("Place the cursor.");
146                    return;
147                };
148
149                let pixel_colors = if let Some(cache) = self
150                    .frame_buffer_pixel_cache
151                    .as_ref()
152                    .filter(|cache| cache.key == pixel_cache_key)
153                {
154                    cache.pixel_colors.clone()
155                } else {
156                    let decoded = if color_settings_key.rgb_mode {
157                        let r_bits = color_settings_key.r_bits as usize;
158                        let g_bits = color_settings_key.g_bits as usize;
159                        let b_bits = color_settings_key.b_bits as usize;
160                        let bits_per_pixel = r_bits + g_bits + b_bits;
161                        if bits_per_pixel == 0 {
162                            ui.label("Set at least one RGB channel bit count above zero.");
163                            return;
164                        }
165                        decode_rgb_pixels(&bits, r_bits, g_bits, b_bits)
166                    } else {
167                        let gray_bits = color_settings_key.grayscale_bits as usize;
168                        decode_grayscale_pixels(&bits, gray_bits)
169                    };
170
171                    self.frame_buffer_pixel_cache = Some(FrameBufferPixelCache {
172                        key: pixel_cache_key,
173                        pixel_colors: decoded.clone(),
174                    });
175                    decoded
176                };
177
178                if pixel_colors.is_empty() {
179                    ui.label("No pixels to draw with current bit settings.");
180                    return;
181                }
182
183                let settings = &mut self.user.frame_buffer;
184                let columns = settings.pixels_per_row.min(pixel_colors.len()).max(1);
185                let rows = pixel_colors.len().div_ceil(columns);
186                ui.horizontal(|ui| {
187                    ui.label(format!("Var: {variable_name} | {columns}×{rows}"));
188
189                    if ui.button("Copy image").clicked() {
190                        let total = columns * rows;
191                        let mut padded = pixel_colors.to_vec();
192                        padded.resize(total, Color32::BLACK);
193                        ui.ctx().copy_image(egui::ColorImage {
194                            size: [columns, rows],
195                            pixels: padded,
196                            source_size: egui::vec2(columns as f32, rows as f32),
197                        });
198                    }
199                });
200                self.draw_array_index_range(ui);
201
202                let settings = &mut self.user.frame_buffer;
203                let max_columns = pixel_colors.len().max(1);
204                settings.pixels_per_row = settings.pixels_per_row.clamp(1, max_columns);
205
206                ui.horizontal(|ui| {
207                    ui.label("Pixels in x-direction");
208                    ui.add(
209                        egui::Slider::new(&mut settings.pixels_per_row, 1..=max_columns).integer(),
210                    );
211                });
212
213                ui.separator();
214
215                let available = ui.available_size_before_wrap();
216
217                if available.x <= 0.0 || available.y <= 0.0 {
218                    return;
219                }
220
221                let (pixel_width, pixel_height) = if settings.square_pixels {
222                    let side = (available.x / columns as f32).min(available.y / rows as f32);
223                    (side, side)
224                } else {
225                    (available.x / columns as f32, available.y / rows as f32)
226                };
227
228                let image_size =
229                    egui::vec2(pixel_width * columns as f32, pixel_height * rows as f32);
230                let (rect, _) = ui.allocate_exact_size(image_size, Sense::hover());
231                let painter = ui.painter_at(rect);
232
233                for (index, color) in pixel_colors.iter().copied().enumerate() {
234                    let x = index % columns;
235                    let y = index / columns;
236
237                    let min = Pos2 {
238                        x: rect.min.x + x as f32 * pixel_width,
239                        y: rect.min.y + y as f32 * pixel_height,
240                    };
241                    let max = Pos2 {
242                        x: min.x + pixel_width,
243                        y: min.y + pixel_height,
244                    };
245
246                    painter.rect_filled(Rect { min, max }, CornerRadius::ZERO, color);
247                }
248
249                painter.rect_stroke(
250                    rect,
251                    CornerRadius::ZERO,
252                    Stroke::new(1.0, ui.visuals().weak_text_color()),
253                    egui::StrokeKind::Inside,
254                );
255            });
256
257        if !open {
258            msgs.push(Message::SetFrameBufferVisibleVariable(None));
259        }
260    }
261
262    fn draw_array_index_range(&mut self, ui: &mut egui::Ui) {
263        let Some(FrameBufferContent::Array {
264            scope_ref: _,
265            levels,
266        }) = self.frame_buffer_content.as_mut()
267        else {
268            return;
269        };
270
271        if levels.is_empty() {
272            return;
273        }
274
275        let total_levels = levels.len();
276
277        for (i, level) in levels.iter_mut().enumerate() {
278            let (min, max) = (level.min_index, level.max_index);
279            level.first_index = level.first_index.clamp(min, max);
280            level.last_index = level.last_index.clamp(min, max);
281            if level.first_index > level.last_index {
282                level.last_index = level.first_index;
283            }
284            ui.horizontal(|ui| {
285                if total_levels == 1 {
286                    ui.label("First array index");
287                } else {
288                    ui.label(format!("Level {} first index", i + 1));
289                }
290                ui.add(DragValue::new(&mut level.first_index).range(min..=max));
291                if total_levels == 1 {
292                    ui.label("Last array index");
293                } else {
294                    ui.label(format!("Level {} last index", i + 1));
295                }
296                ui.add(DragValue::new(&mut level.last_index).range(min..=max));
297            });
298            if level.first_index > level.last_index {
299                level.first_index = level.last_index;
300            }
301        }
302    }
303
304    fn selected_variable_for_frame_buffer(&mut self) -> Option<(VariableValue, u32, String)> {
305        let waves = self.user.waves.as_ref()?;
306        let cursor = waves.cursor.as_ref()?.to_biguint()?;
307        let wave_container = waves.inner.as_waves()?;
308        let content = self.frame_buffer_content.clone()?;
309        let cache_key = FrameBufferContentCacheKey {
310            content: content.clone(),
311            cursor_position: cursor.clone(),
312        };
313        let cached = self
314            .frame_buffer_array_cache
315            .as_ref()
316            .filter(|cache| cache.key == cache_key)
317            .cloned();
318
319        let cached = if let Some(cached) = cached {
320            cached
321        } else {
322            let cached = match &content {
323                FrameBufferContent::Variable(variable_ref) => build_variable_frame_buffer_cache(
324                    wave_container,
325                    variable_ref,
326                    &cursor,
327                    cache_key,
328                )?,
329                FrameBufferContent::Array { scope_ref, levels } => {
330                    if levels.is_empty() {
331                        return None;
332                    }
333
334                    let sorted_variables =
335                        resolve_leaf_scopes_and_variables(wave_container, scope_ref, levels)?;
336                    let cached_value =
337                        build_cached_variable_value(wave_container, &sorted_variables, &cursor);
338                    FrameBufferArrayCache {
339                        key: cache_key,
340                        cached_value,
341                    }
342                }
343            };
344            self.frame_buffer_array_cache = Some(cached.clone());
345            cached
346        };
347
348        let (concat_bits, total_bits) = cached.cached_value.as_ref()?;
349
350        let variable_name = match &content {
351            FrameBufferContent::Variable(variable_ref) => variable_ref.full_path_string_no_index(),
352            FrameBufferContent::Array { scope_ref, .. } => scope_ref.full_name(),
353        };
354
355        Some((
356            VariableValue::String(concat_bits.clone()),
357            *total_bits,
358            variable_name,
359        ))
360    }
361
362    fn current_frame_buffer_array_cache_key(&self) -> Option<FrameBufferContentCacheKey> {
363        let waves = self.user.waves.as_ref()?;
364        let cursor_position = waves.cursor.as_ref()?.to_biguint()?;
365        let content = self.frame_buffer_content.clone()?;
366        Some(FrameBufferContentCacheKey {
367            content,
368            cursor_position,
369        })
370    }
371}
372
373fn build_cached_variable_value(
374    wave_container: &WaveContainer,
375    sorted_variables: &[VariableRef],
376    cursor: &num::BigUint,
377) -> Option<(String, u32)> {
378    let mut concat_bits = String::new();
379    let mut total_bits: u32 = 0;
380    for var_ref in sorted_variables {
381        let meta = wave_container.variable_meta(var_ref).ok()?;
382        let bits = meta.num_bits? as usize;
383        total_bits += bits as u32;
384        let query_result = wave_container
385            .query_variable(var_ref, cursor)
386            .ok()
387            .flatten()?;
388        let (_, value) = query_result.current?;
389        let bit_str = match &value {
390            VariableValue::BigUint(v) => format!("{v:b}"),
391            VariableValue::String(s) => s.clone(),
392        };
393        let padded = if bit_str.len() < bits {
394            format!("{bit_str:0>bits$}")
395        } else {
396            bit_str[bit_str.len() - bits..].to_string()
397        };
398        concat_bits.push_str(&padded);
399    }
400    if total_bits == 0 {
401        None
402    } else {
403        Some((concat_bits, total_bits))
404    }
405}
406
407fn build_variable_frame_buffer_cache(
408    wave_container: &WaveContainer,
409    variable_ref: &VariableRef,
410    cursor: &num::BigUint,
411    key: FrameBufferContentCacheKey,
412) -> Option<FrameBufferArrayCache> {
413    let meta = wave_container.variable_meta(variable_ref).ok()?;
414    let word_length = meta.num_bits? as usize;
415    let query_result = wave_container
416        .query_variable(variable_ref, cursor)
417        .ok()
418        .flatten()?;
419    let (_, value) = query_result.current?;
420    let bits = match value {
421        VariableValue::BigUint(v) => format!("{v:b}"),
422        VariableValue::String(s) => s,
423    };
424    let padded = if bits.len() < word_length {
425        format!("{bits:0>word_length$}")
426    } else {
427        bits[bits.len() - word_length..].to_string()
428    };
429
430    Some(FrameBufferArrayCache {
431        key,
432        cached_value: Some((padded, word_length as u32)),
433    })
434}
435
436fn resolve_leaf_scopes_and_variables(
437    wave_container: &WaveContainer,
438    scope_ref: &ScopeRef,
439    levels: &[ArrayLevel],
440) -> Option<Vec<VariableRef>> {
441    let (scope_levels, var_level) = levels.split_at(levels.len() - 1);
442    let var_level = &var_level[0];
443
444    let mut current_scopes = vec![scope_ref.clone()];
445    for level in scope_levels {
446        let clamped_first = level.first_index.clamp(level.min_index, level.max_index);
447        let clamped_last = level.last_index.clamp(level.min_index, level.max_index);
448        let mut next_scopes = Vec::new();
449        for scope in &current_scopes {
450            let mut selected: Vec<ScopeRef> = wave_container
451                .child_scopes(scope)
452                .unwrap_or_default()
453                .into_iter()
454                .filter(|s| {
455                    let idx = scope_array_index(s);
456                    idx >= clamped_first && idx <= clamped_last
457                })
458                .collect();
459            selected.sort_by_key(scope_array_index);
460            next_scopes.extend(selected);
461        }
462        current_scopes = next_scopes;
463    }
464
465    if current_scopes.is_empty() {
466        return None;
467    }
468
469    let clamped_first = var_level
470        .first_index
471        .clamp(var_level.min_index, var_level.max_index);
472    let clamped_last = var_level
473        .last_index
474        .clamp(var_level.min_index, var_level.max_index);
475    if clamped_first > clamped_last {
476        return None;
477    }
478
479    let mut sorted_variables = Vec::new();
480    for leaf_scope in &current_scopes {
481        let mut variables = wave_container.variables_in_scope(leaf_scope);
482        variables.sort_by_key(variable_array_index);
483        sorted_variables.extend(variables.into_iter().filter(|var_ref| {
484            let idx = variable_array_index(var_ref);
485            idx >= clamped_first && idx <= clamped_last
486        }));
487    }
488
489    Some(sorted_variables)
490}
491
492/// Analyses the scope hierarchy rooted at `scope_ref` and returns:
493/// - `levels`: one `ArrayLevel` per nesting level, where the last level is for variables
494/// - `all_leaf_vars`: every variable reachable from the root (for pre-loading)
495///
496/// Returns `None` when `scope_ref` is not found in the hierarchy.
497pub(crate) fn build_frame_buffer_content(
498    wave_container: &WaveContainer,
499    scope_ref: &ScopeRef,
500) -> Option<(Vec<ArrayLevel>, Vec<VariableRef>)> {
501    // Probe the hierarchy by following the min-index child at each level.
502    // Stop when we reach a leaf scope that has no child scopes.
503    let mut levels: Vec<ArrayLevel> = Vec::new();
504    let mut probe = scope_ref.clone();
505    loop {
506        let children = wave_container.child_scopes(&probe).unwrap_or_default();
507        if children.is_empty() {
508            break;
509        }
510        let indices: Vec<i64> = children.iter().map(scope_array_index).collect();
511        let min_idx = *indices.iter().min().unwrap_or(&0);
512        let max_idx = *indices.iter().max().unwrap_or(&0);
513        levels.push(ArrayLevel {
514            min_index: min_idx,
515            max_index: max_idx,
516            first_index: min_idx,
517            last_index: max_idx,
518        });
519        probe = children.into_iter().min_by_key(scope_array_index).unwrap();
520    }
521
522    // Determine the variable index range from the representative leaf scope.
523    let leaf_vars = wave_container.variables_in_scope(&probe);
524    let var_indices: Vec<i64> = leaf_vars
525        .iter()
526        .map(variable_array_index)
527        .filter(|&i| i != i64::MAX)
528        .collect();
529    let (var_min, var_max) = if var_indices.is_empty() {
530        (0, 0)
531    } else {
532        (
533            *var_indices.iter().min().unwrap(),
534            *var_indices.iter().max().unwrap(),
535        )
536    };
537    levels.push(ArrayLevel {
538        min_index: var_min,
539        max_index: var_max,
540        first_index: var_min,
541        last_index: var_max,
542    });
543
544    // Walk every path to collect all leaf variables for pre-loading.
545    let depth = levels.len().saturating_sub(1);
546    let mut leaf_scopes = vec![scope_ref.clone()];
547    for _ in 0..depth {
548        leaf_scopes = leaf_scopes
549            .iter()
550            .flat_map(|s| wave_container.child_scopes(s).unwrap_or_default())
551            .collect();
552    }
553    let all_leaf_vars: Vec<VariableRef> = leaf_scopes
554        .iter()
555        .flat_map(|s| wave_container.variables_in_scope(s))
556        .collect();
557
558    Some((levels, all_leaf_vars))
559}
560
561fn scope_array_index(scope_ref: &ScopeRef) -> i64 {
562    let name = scope_ref.name();
563    name.parse::<i64>()
564        .ok()
565        .or_else(|| {
566            name.strip_prefix('[')
567                .and_then(|s| s.strip_suffix(']'))
568                .and_then(|s| s.parse::<i64>().ok())
569        })
570        .unwrap_or(i64::MAX)
571}
572
573fn variable_array_index(var_ref: &VariableRef) -> i64 {
574    fn parse_index_name(name: &str) -> Option<i64> {
575        name.parse::<i64>().ok().or_else(|| {
576            name.strip_prefix('[')
577                .and_then(|s| s.strip_suffix(']'))
578                .and_then(|s| s.parse::<i64>().ok())
579        })
580    }
581
582    var_ref
583        .index
584        .or_else(|| parse_index_name(&var_ref.name))
585        .unwrap_or(i64::MAX)
586}
587
588fn frame_buffer_bits(value: &VariableValue, word_length: usize) -> Vec<bool> {
589    let mut bits: Vec<bool> = match value {
590        VariableValue::BigUint(v) => format!("{v:b}").chars().map(|c| c == '1').collect(),
591        VariableValue::String(v) => v.chars().map(|c| c == '1').collect(),
592    };
593
594    if bits.len() < word_length {
595        let mut padded = vec![false; word_length - bits.len()];
596        padded.extend(bits);
597        bits = padded;
598    } else if bits.len() > word_length {
599        bits = bits[bits.len() - word_length..].to_vec();
600    }
601
602    bits
603}
604
605fn decode_grayscale_pixels(bits: &[bool], grayscale_bits: usize) -> Vec<Color32> {
606    let mut out = Vec::with_capacity(bits.len().div_ceil(grayscale_bits.max(1)));
607    for start in (0..bits.len()).step_by(grayscale_bits.max(1)) {
608        let gray = scale_to_u8(
609            bits_to_u16_padded(bits, start, grayscale_bits),
610            grayscale_bits,
611        );
612        out.push(Color32::from_rgb(gray, gray, gray));
613    }
614    out
615}
616
617fn decode_rgb_pixels(bits: &[bool], r_bits: usize, g_bits: usize, b_bits: usize) -> Vec<Color32> {
618    let bits_per_pixel = r_bits + g_bits + b_bits;
619    let mut out = Vec::with_capacity(bits.len().div_ceil(bits_per_pixel.max(1)));
620    for start in (0..bits.len()).step_by(bits_per_pixel.max(1)) {
621        let red = scale_to_u8(bits_to_u16_padded(bits, start, r_bits), r_bits);
622        let green = scale_to_u8(bits_to_u16_padded(bits, start + r_bits, g_bits), g_bits);
623        let blue = scale_to_u8(
624            bits_to_u16_padded(bits, start + r_bits + g_bits, b_bits),
625            b_bits,
626        );
627        out.push(Color32::from_rgb(red, green, blue));
628    }
629    out
630}
631
632fn bits_to_u16_padded(bits: &[bool], start: usize, len: usize) -> u16 {
633    let mut value = 0u16;
634    for offset in 0..len {
635        value = (value << 1) | u16::from(bits.get(start + offset).copied().unwrap_or(false));
636    }
637    value
638}
639
640fn scale_to_u8(value: u16, bits: usize) -> u8 {
641    if bits == 0 {
642        return 0;
643    }
644    let max_in = (1u16 << bits) - 1;
645    (((value as u32) * 255) / (max_in as u32)) as u8
646}
647
648#[cfg(test)]
649mod tests {
650    use super::*;
651    use num::BigUint;
652
653    #[test]
654    fn frame_buffer_bits_pads_to_word_length() {
655        let bits = frame_buffer_bits(&VariableValue::BigUint(BigUint::from(0b101u8)), 5);
656        assert_eq!(bits, vec![false, false, true, false, true]);
657    }
658
659    #[test]
660    fn frame_buffer_bits_truncates_to_word_length() {
661        let bits = frame_buffer_bits(&VariableValue::String("101101".to_string()), 4);
662        assert_eq!(bits, vec![true, true, false, true]);
663    }
664
665    #[test]
666    fn bits_to_u16_padded_reads_and_zero_pads() {
667        let bits = vec![true, false, true];
668        assert_eq!(bits_to_u16_padded(&bits, 0, 3), 0b101);
669        assert_eq!(bits_to_u16_padded(&bits, 1, 4), 0b0100);
670    }
671
672    #[test]
673    fn scale_to_u8_scales_full_range() {
674        assert_eq!(scale_to_u8(0, 1), 0);
675        assert_eq!(scale_to_u8(1, 1), 255);
676        assert_eq!(scale_to_u8(7, 3), 255);
677        assert_eq!(scale_to_u8(4, 3), 145);
678    }
679
680    #[test]
681    fn decode_grayscale_pixels_uses_bit_groups() {
682        let bits = vec![false, false, true, true];
683        let pixels = decode_grayscale_pixels(&bits, 2);
684        assert_eq!(pixels.len(), 2);
685        assert_eq!(pixels[0], Color32::from_rgb(0, 0, 0));
686        assert_eq!(pixels[1], Color32::from_rgb(255, 255, 255));
687    }
688
689    #[test]
690    fn decode_rgb_pixels_supports_different_channel_widths() {
691        let bits = vec![
692            true, false, false, true, true, false, // R=10 G=01 B=10 with r=2,g=2,b=2
693        ];
694        let pixels = decode_rgb_pixels(&bits, 2, 2, 2);
695        assert_eq!(pixels.len(), 1);
696        assert_eq!(pixels[0], Color32::from_rgb(170, 85, 170));
697    }
698
699    #[test]
700    fn variable_array_index_parses_bracketed_name() {
701        let var_ref = VariableRef::new(ScopeRef::empty(), "[2]".to_string());
702        assert_eq!(variable_array_index(&var_ref), 2);
703    }
704
705    #[test]
706    fn variable_array_index_parses_plain_numeric_name() {
707        let var_ref = VariableRef::new(ScopeRef::empty(), "7".to_string());
708        assert_eq!(variable_array_index(&var_ref), 7);
709    }
710
711    #[test]
712    fn variable_array_index_prefers_explicit_index() {
713        let var_ref = VariableRef::new_with_id_and_index(
714            ScopeRef::empty(),
715            "[2]".to_string(),
716            Default::default(),
717            Some(9),
718        );
719        assert_eq!(variable_array_index(&var_ref), 9);
720    }
721
722    #[test]
723    fn variable_array_index_falls_back_to_max_for_non_numeric_names() {
724        let var_ref = VariableRef::new(ScopeRef::empty(), "data".to_string());
725        assert_eq!(variable_array_index(&var_ref), i64::MAX);
726    }
727}