1use crate::mapping_slots::MappingSlots;
7use alloy_dyn_abi::{DynSolType, DynSolValue};
8use alloy_primitives::{B256, U256, hex, keccak256, map::B256Map};
9use foundry_common_fmt::format_token_raw;
10use foundry_compilers::artifacts::{Storage, StorageLayout, StorageType};
11use serde::Serialize;
12use std::{collections::BTreeMap, str::FromStr, sync::Arc};
13use tracing::trace;
14
15pub const ENCODING_INPLACE: &str = "inplace";
17pub const ENCODING_MAPPING: &str = "mapping";
19pub const ENCODING_BYTES: &str = "bytes";
22pub const ENCODING_DYN_ARRAY: &str = "dynamic_array";
24
25#[derive(Clone, Serialize, Debug)]
27pub struct SlotInfo {
28 pub label: String,
36 #[serde(rename = "type", serialize_with = "serialize_slot_type")]
38 pub slot_type: StorageTypeInfo,
39 pub offset: i64,
41 pub slot: String,
43 #[serde(skip_serializing_if = "Option::is_none")]
47 pub members: Option<Vec<Self>>,
48 #[serde(skip_serializing_if = "Option::is_none")]
50 pub decoded: Option<DecodedSlotValues>,
51 #[serde(
53 skip_serializing_if = "Option::is_none",
54 flatten,
55 serialize_with = "serialize_mapping_keys"
56 )]
57 pub keys: Option<Vec<String>>,
58}
59
60#[derive(Clone, Debug)]
66pub struct StorageTypeInfo {
67 pub label: String,
70 pub dyn_sol_type: DynSolType,
72}
73
74impl SlotInfo {
75 pub fn decode(&self, value: B256) -> Option<DynSolValue> {
80 let mut actual_type = &self.slot_type.dyn_sol_type;
82 while let DynSolType::FixedArray(elem_type, _) = actual_type {
84 actual_type = elem_type.as_ref();
85 }
86
87 match actual_type {
89 DynSolType::Bytes | DynSolType::String => {
90 let length_byte = value.0[31];
94
95 if length_byte & 1 == 0 {
96 let length = (length_byte >> 1) as usize;
98 let data = if length == 0 { Vec::new() } else { value.0[0..length].to_vec() };
100
101 if matches!(actual_type, DynSolType::String) {
103 let str_val = if data.is_empty() {
104 String::new()
105 } else {
106 String::from_utf8(data).unwrap_or_default()
107 };
108 Some(DynSolValue::String(str_val))
109 } else {
110 Some(DynSolValue::Bytes(data))
111 }
112 } else {
113 None
117 }
118 }
119 _ => {
120 actual_type.abi_decode(&value.0).ok()
122 }
123 }
124 }
125
126 pub const fn is_bytes_or_string(&self) -> bool {
128 matches!(self.slot_type.dyn_sol_type, DynSolType::Bytes | DynSolType::String)
129 }
130
131 pub fn decode_bytes_or_string(
134 &mut self,
135 base_slot: &B256,
136 storage_values: &B256Map<B256>,
137 ) -> Option<DynSolValue> {
138 if !self.is_bytes_or_string() {
140 return None;
141 }
142
143 self.aggregate_bytes_or_strings(base_slot, storage_values).map(|data| {
145 match self.slot_type.dyn_sol_type {
146 DynSolType::String => {
147 DynSolValue::String(String::from_utf8(data).unwrap_or_default())
148 }
149 DynSolType::Bytes => DynSolValue::Bytes(data),
150 _ => unreachable!(),
151 }
152 })
153 }
154
155 pub fn decode_bytes_or_string_values(
160 &mut self,
161 base_slot: &B256,
162 storage_accesses: &BTreeMap<B256, (B256, B256)>,
163 ) {
164 if !self.is_bytes_or_string() {
166 return;
167 }
168
169 if let Some((prev_base_value, new_base_value)) = storage_accesses.get(base_slot) {
171 let mut decode_value = |base_value: B256, is_new: bool| {
173 let length_byte = base_value.0[31];
174 if length_byte & 1 == 1 {
175 let value_map = storage_accesses
177 .iter()
178 .map(|(slot, (prev, new))| (*slot, if is_new { *new } else { *prev }))
179 .collect::<B256Map<_>>();
180 self.decode_bytes_or_string(base_slot, &value_map)
181 } else {
182 self.decode(base_value)
184 }
185 };
186
187 let prev_decoded = decode_value(*prev_base_value, false);
189
190 let new_decoded = decode_value(*new_base_value, true);
192
193 if let (Some(prev), Some(new)) = (prev_decoded, new_decoded) {
195 self.decoded = Some(DecodedSlotValues { previous_value: prev, new_value: new });
196 }
197 }
198 }
199
200 fn aggregate_bytes_or_strings(
205 &mut self,
206 base_slot: &B256,
207 storage_values: &B256Map<B256>,
208 ) -> Option<Vec<u8>> {
209 if !self.is_bytes_or_string() {
210 return None;
211 }
212
213 if let Some(base_value) = storage_values.get(base_slot) {
215 let length_byte = base_value.0[31];
216
217 if length_byte & 1 == 1 {
219 let length: U256 = U256::from_be_bytes(base_value.0) >> 1;
221 let num_slots = length.to::<usize>().div_ceil(32).min(256);
222 let data_start = U256::from_be_bytes(keccak256(base_slot.0).0);
223
224 let mut members = Vec::new();
225 let mut full_data = Vec::with_capacity(length.to::<usize>());
226
227 for i in 0..num_slots {
228 let data_slot = B256::from(data_start + U256::from(i));
229 let data_slot_u256 = data_start + U256::from(i);
230
231 let member_info = Self {
233 label: format!("{}[{}]", self.label, i),
234 slot_type: StorageTypeInfo {
235 label: self.slot_type.label.clone(),
236 dyn_sol_type: DynSolType::FixedBytes(32),
237 },
238 offset: 0,
239 slot: data_slot_u256.to_string(),
240 members: None,
241 decoded: None,
242 keys: None,
243 };
244
245 if let Some(value) = storage_values.get(&data_slot) {
246 let bytes_to_take =
248 std::cmp::min(32, length.to::<usize>() - full_data.len());
249 full_data.extend_from_slice(&value.0[..bytes_to_take]);
250 }
251
252 members.push(member_info);
253 }
254
255 if !members.is_empty() {
257 self.members = Some(members);
258 }
259
260 return Some(full_data);
261 }
262 }
263
264 None
265 }
266
267 pub fn decode_values(&mut self, previous_value: B256, new_value: B256) {
270 if let Some(members) = &mut self.members {
272 for member in members.iter_mut() {
273 let offset = member.offset as usize;
274 let size = match &member.slot_type.dyn_sol_type {
275 DynSolType::Uint(bits) | DynSolType::Int(bits) => bits / 8,
276 DynSolType::Address => 20,
277 DynSolType::Bool => 1,
278 DynSolType::FixedBytes(size) => *size,
279 _ => 32, };
281
282 let mut prev_bytes = [0u8; 32];
284 let mut new_bytes = [0u8; 32];
285
286 if offset + size <= 32 {
287 let byte_start = 32 - offset - size;
294 prev_bytes[32 - size..]
295 .copy_from_slice(&previous_value.0[byte_start..byte_start + size]);
296 new_bytes[32 - size..]
297 .copy_from_slice(&new_value.0[byte_start..byte_start + size]);
298 }
299
300 if let (Ok(prev_val), Ok(new_val)) = (
302 member.slot_type.dyn_sol_type.abi_decode(&prev_bytes),
303 member.slot_type.dyn_sol_type.abi_decode(&new_bytes),
304 ) {
305 member.decoded =
306 Some(DecodedSlotValues { previous_value: prev_val, new_value: new_val });
307 }
308 }
309 } else {
311 if let (Some(prev), Some(new)) = (self.decode(previous_value), self.decode(new_value)) {
315 self.decoded = Some(DecodedSlotValues { previous_value: prev, new_value: new });
316 }
317 }
318 }
319}
320
321fn serialize_slot_type<S>(info: &StorageTypeInfo, serializer: S) -> Result<S::Ok, S::Error>
323where
324 S: serde::Serializer,
325{
326 serializer.serialize_str(&info.label)
327}
328
329fn serialize_mapping_keys<S>(keys: &Option<Vec<String>>, serializer: S) -> Result<S::Ok, S::Error>
331where
332 S: serde::Serializer,
333{
334 use serde::ser::SerializeMap;
335
336 if let Some(keys) = keys {
337 let len = if keys.is_empty() { 0 } else { 1 };
338 let mut map = serializer.serialize_map(Some(len))?;
339 if keys.len() == 1 {
340 map.serialize_entry("key", &keys[0])?;
341 } else if keys.len() > 1 {
342 map.serialize_entry("keys", keys)?;
343 }
344 map.end()
345 } else {
346 serializer.serialize_none()
347 }
348}
349
350#[derive(Clone, Debug)]
352pub struct DecodedSlotValues {
353 pub previous_value: DynSolValue,
355 pub new_value: DynSolValue,
357}
358
359impl Serialize for DecodedSlotValues {
360 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
361 where
362 S: serde::Serializer,
363 {
364 use serde::ser::SerializeStruct;
365
366 let mut state = serializer.serialize_struct("DecodedSlotValues", 2)?;
367 state.serialize_field("previousValue", &format_token_raw(&self.previous_value))?;
368 state.serialize_field("newValue", &format_token_raw(&self.new_value))?;
369 state.end()
370 }
371}
372
373#[derive(Clone)]
375pub struct SlotIdentifier {
376 storage_layout: Arc<StorageLayout>,
377 parsed_types: BTreeMap<String, Option<DynSolType>>,
378}
379
380impl SlotIdentifier {
381 pub fn new(storage_layout: Arc<StorageLayout>) -> Self {
383 let parsed_types = storage_layout
384 .types
385 .iter()
386 .map(|(id, storage_type)| (id.clone(), DynSolType::parse(&storage_type.label).ok()))
387 .collect();
388 Self { storage_layout, parsed_types }
389 }
390
391 fn parsed_type(&self, storage_type: &str) -> Option<&DynSolType> {
392 self.parsed_types.get(storage_type).and_then(Option::as_ref)
393 }
394
395 pub fn identify(&self, slot: &B256, mapping_slots: Option<&MappingSlots>) -> Option<SlotInfo> {
399 trace!(?slot, "identifying slot");
400 let slot_u256 = U256::from_be_bytes(slot.0);
401 let slot_str = slot_u256.to_string();
402
403 for storage in &self.storage_layout.storage {
404 let storage_type = self.storage_layout.types.get(&storage.storage_type)?;
405 let dyn_type = self.parsed_type(&storage.storage_type);
406
407 if storage.slot == slot_str
410 && let Some(parsed_type) = dyn_type.cloned()
411 {
412 let label = if let DynSolType::FixedArray(_, _) = &parsed_type {
414 format!("{}{}", storage.label, get_array_base_indices(&parsed_type))
415 } else {
416 storage.label.clone()
417 };
418
419 return Some(SlotInfo {
420 label,
421 slot_type: StorageTypeInfo {
422 label: storage_type.label.clone(),
423 dyn_sol_type: parsed_type,
424 },
425 offset: storage.offset,
426 slot: storage.slot.clone(),
427 members: None,
428 decoded: None,
429 keys: None,
430 });
431 }
432
433 if storage_type.encoding == ENCODING_INPLACE {
435 let array_start_slot = U256::from_str(&storage.slot).ok()?;
438
439 if let Some(parsed_type) = dyn_type
440 && let DynSolType::FixedArray(_, _) = parsed_type
441 && let Some(slot_info) = self.handle_array_slot(
442 storage,
443 storage_type,
444 parsed_type,
445 slot_u256,
446 array_start_slot,
447 &slot_str,
448 )
449 {
450 return Some(slot_info);
451 }
452
453 if is_struct(&storage_type.label) {
455 let struct_start_slot = U256::from_str(&storage.slot).ok()?;
456 if let Some(slot_info) = self.handle_struct(
457 &storage.label,
458 storage_type,
459 slot_u256,
460 struct_start_slot,
461 storage.offset,
462 &slot_str,
463 0,
464 ) {
465 return Some(slot_info);
466 }
467 }
468 } else if storage_type.encoding == ENCODING_MAPPING
469 && let Some(mapping_slots) = mapping_slots
470 && let Some(info) =
471 self.handle_mapping(storage, storage_type, slot, &slot_str, mapping_slots)
472 {
473 return Some(info);
474 }
475 }
476
477 None
478 }
479
480 pub fn identify_bytes_or_string(
487 &self,
488 slot: &B256,
489 storage_values: &B256Map<B256>,
490 ) -> Option<SlotInfo> {
491 let slot_u256 = U256::from_be_bytes(slot.0);
492 let slot_str = slot_u256.to_string();
493
494 for storage in &self.storage_layout.storage {
496 if let Some(storage_type) = self.storage_layout.types.get(&storage.storage_type)
497 && storage_type.encoding == ENCODING_BYTES
498 {
499 let Some(base_slot) = U256::from_str(&storage.slot).map(B256::from).ok() else {
500 continue;
501 };
502 if let Some(base_value) = storage_values.get(&base_slot)
504 && let Some(info) = self.handle_bytes_string(
505 storage,
506 storage_type,
507 slot_u256,
508 &slot_str,
509 base_value,
510 )
511 {
512 return Some(info);
513 }
514 }
515 }
516
517 None
518 }
519
520 fn handle_array_slot(
529 &self,
530 storage: &Storage,
531 storage_type: &StorageType,
532 parsed_type: &DynSolType,
533 slot: U256,
534 array_start_slot: U256,
535 slot_str: &str,
536 ) -> Option<SlotInfo> {
537 let total_bytes = storage_type.number_of_bytes.parse::<u64>().ok()?;
539 let total_slots = total_bytes.div_ceil(32);
540
541 if slot >= array_start_slot && slot < array_start_slot + U256::from(total_slots) {
542 let index = (slot - array_start_slot).to::<u64>();
543 let label = match parsed_type {
545 DynSolType::FixedArray(inner, _) => {
546 if let DynSolType::FixedArray(_, inner_size) = inner.as_ref() {
547 let row = index / (*inner_size as u64);
549 let col = index % (*inner_size as u64);
550 format!("{}[{row}][{col}]", storage.label)
551 } else {
552 format!("{}[{index}]", storage.label)
554 }
555 }
556 _ => storage.label.clone(),
557 };
558
559 return Some(SlotInfo {
560 label,
561 slot_type: StorageTypeInfo {
562 label: storage_type.label.clone(),
563 dyn_sol_type: parsed_type.clone(),
564 },
565 offset: 0,
566 slot: slot_str.to_string(),
567 members: None,
568 decoded: None,
569 keys: None,
570 });
571 }
572
573 None
574 }
575
576 #[allow(clippy::too_many_arguments)]
590 fn handle_struct(
591 &self,
592 base_label: &str,
593 storage_type: &StorageType,
594 target_slot: U256,
595 struct_start_slot: U256,
596 offset: i64,
597 slot_str: &str,
598 depth: usize,
599 ) -> Option<SlotInfo> {
600 const MAX_DEPTH: usize = 10;
602 if depth > MAX_DEPTH {
603 return None;
604 }
605
606 let members = storage_type
607 .other
608 .get("members")
609 .and_then(|v| serde_json::from_value::<Vec<Storage>>(v.clone()).ok())?;
610
611 if struct_start_slot == target_slot
613 && let Some(first_member) = members.iter().find(|m| m.slot == "0")
615 {
616 let member_type_info = self.storage_layout.types.get(&first_member.storage_type)?;
617
618 let is_single_slot = members.iter().all(|m| m.slot == "0");
620
621 if is_single_slot {
622 let mut member_infos = Vec::new();
624 for member in &members {
625 if let Some(member_type_info) =
626 self.storage_layout.types.get(&member.storage_type)
627 && let Some(member_type) = self.parsed_type(&member.storage_type).cloned()
628 {
629 member_infos.push(SlotInfo {
630 label: member.label.clone(),
631 slot_type: StorageTypeInfo {
632 label: member_type_info.label.clone(),
633 dyn_sol_type: member_type,
634 },
635 offset: member.offset,
636 slot: slot_str.to_string(),
637 members: None,
638 decoded: None,
639 keys: None,
640 });
641 }
642 }
643
644 let struct_name =
646 storage_type.label.strip_prefix("struct ").unwrap_or(&storage_type.label);
647 let prop_names: Vec<String> = members.iter().map(|m| m.label.clone()).collect();
648 let member_types: Vec<DynSolType> =
649 member_infos.iter().map(|info| info.slot_type.dyn_sol_type.clone()).collect();
650
651 let parsed_type = DynSolType::CustomStruct {
652 name: struct_name.to_string(),
653 prop_names,
654 tuple: member_types,
655 };
656
657 return Some(SlotInfo {
658 label: base_label.to_string(),
659 slot_type: StorageTypeInfo {
660 label: storage_type.label.clone(),
661 dyn_sol_type: parsed_type,
662 },
663 offset,
664 slot: slot_str.to_string(),
665 decoded: None,
666 members: if member_infos.is_empty() { None } else { Some(member_infos) },
667 keys: None,
668 });
669 }
670
671 let member_label = format!("{}.{}", base_label, first_member.label);
673
674 if is_struct(&member_type_info.label) {
676 return self.handle_struct(
677 &member_label,
678 member_type_info,
679 target_slot,
680 struct_start_slot,
681 first_member.offset,
682 slot_str,
683 depth + 1,
684 );
685 }
686
687 return Some(SlotInfo {
689 label: member_label,
690 slot_type: StorageTypeInfo {
691 label: member_type_info.label.clone(),
692 dyn_sol_type: self.parsed_type(&first_member.storage_type)?.clone(),
693 },
694 offset: first_member.offset,
695 slot: slot_str.to_string(),
696 decoded: None,
697 members: None,
698 keys: None,
699 });
700 }
701
702 for member in &members {
704 let member_slot_offset = U256::from_str(&member.slot).ok()?;
705 let member_slot = struct_start_slot + member_slot_offset;
706 let member_type_info = self.storage_layout.types.get(&member.storage_type)?;
707 let member_label = format!("{}.{}", base_label, member.label);
708
709 if is_struct(&member_type_info.label) {
711 let slot_info = self.handle_struct(
712 &member_label,
713 member_type_info,
714 target_slot,
715 member_slot,
716 member.offset,
717 slot_str,
718 depth + 1,
719 );
720
721 if member_slot == target_slot || slot_info.is_some() {
722 return slot_info;
723 }
724 }
725
726 if member_slot == target_slot {
727 let member_type = self.parsed_type(&member.storage_type)?.clone();
731 return Some(SlotInfo {
732 label: member_label,
733 slot_type: StorageTypeInfo {
734 label: member_type_info.label.clone(),
735 dyn_sol_type: member_type,
736 },
737 offset: member.offset,
738 slot: slot_str.to_string(),
739 members: None,
740 decoded: None,
741 keys: None,
742 });
743 }
744 }
745
746 None
747 }
748
749 fn handle_mapping(
761 &self,
762 storage: &Storage,
763 storage_type: &StorageType,
764 slot: &B256,
765 slot_str: &str,
766 mapping_slots: &MappingSlots,
767 ) -> Option<SlotInfo> {
768 trace!(
769 "handle_mapping: storage.slot={}, slot={:?}, has_keys={}, has_parents={}",
770 storage.slot,
771 slot,
772 mapping_slots.keys.contains_key(slot),
773 mapping_slots.parent_slots.contains_key(slot)
774 );
775
776 if storage_type.encoding != ENCODING_MAPPING {
778 return None;
779 }
780
781 if !mapping_slots.keys.contains_key(slot) {
783 return None;
784 }
785
786 let storage_slot_b256 = B256::from(U256::from_str(&storage.slot).ok()?);
788
789 let mut current_slot = *slot;
791 let mut keys_to_decode = Vec::new();
792 let mut found_base = false;
793
794 while let Some((key, parent)) =
795 mapping_slots.keys.get(¤t_slot).zip(mapping_slots.parent_slots.get(¤t_slot))
796 {
797 keys_to_decode.push(*key);
798
799 if *parent == storage_slot_b256 {
801 found_base = true;
802 break;
803 }
804
805 current_slot = *parent;
807 }
808
809 if !found_base {
810 trace!("Mapping slot {} does not match any parent in chain", storage.slot);
811 return None;
812 }
813
814 let (key_types, value_type_label, full_type_label) =
816 self.resolve_mapping_type(&storage.storage_type)?;
817
818 keys_to_decode.reverse();
820
821 let mut label = storage.label.clone();
823 let mut decoded_keys = Vec::new();
824
825 for (i, key) in keys_to_decode.iter().enumerate() {
827 if let Some(key_type_label) = key_types.get(i)
828 && let Ok(sol_type) = DynSolType::parse(key_type_label)
829 && let Ok(decoded) = sol_type.abi_decode(&key.0)
830 {
831 let decoded_key_str = format_token_raw(&decoded);
832 decoded_keys.push(decoded_key_str.clone());
833 label = format!("{label}[{decoded_key_str}]");
834 } else {
835 let hex_key = hex::encode_prefixed(key.0);
836 decoded_keys.push(hex_key.clone());
837 label = format!("{label}[{hex_key}]");
838 }
839 }
840
841 let dyn_sol_type = DynSolType::parse(&value_type_label).unwrap_or(DynSolType::Bytes);
843
844 Some(SlotInfo {
845 label,
846 slot_type: StorageTypeInfo { label: full_type_label, dyn_sol_type },
847 offset: storage.offset,
848 slot: slot_str.to_string(),
849 members: None,
850 decoded: None,
851 keys: Some(decoded_keys),
852 })
853 }
854
855 fn handle_bytes_string(
871 &self,
872 storage: &Storage,
873 storage_type: &StorageType,
874 slot: U256,
875 slot_str: &str,
876 base_slot_value: &B256,
877 ) -> Option<SlotInfo> {
878 if storage_type.encoding != ENCODING_BYTES {
880 return None;
881 }
882
883 let base_slot = U256::from_str(&storage.slot).ok()?;
885 if slot == base_slot {
886 let dyn_type = if storage_type.label == "string" {
888 DynSolType::String
889 } else if storage_type.label == "bytes" {
890 DynSolType::Bytes
891 } else {
892 return None;
893 };
894
895 return Some(SlotInfo {
896 label: storage.label.clone(),
897 slot_type: StorageTypeInfo {
898 label: storage_type.label.clone(),
899 dyn_sol_type: dyn_type,
900 },
901 offset: storage.offset,
902 slot: slot_str.to_string(),
903 members: None,
904 decoded: None,
905 keys: None,
906 });
907 }
908
909 let data_start =
912 U256::from_be_bytes(alloy_primitives::keccak256(base_slot.to_be_bytes::<32>()).0);
913
914 let length_byte = base_slot_value.0[31];
917 if length_byte & 1 == 1 {
918 let length = U256::from_be_bytes(base_slot_value.0) >> 1;
920 let num_slots = (length + U256::from(31)) / U256::from(32);
922
923 if slot >= data_start && slot < data_start + num_slots {
925 let slot_index = (slot - data_start).to::<usize>();
926
927 return Some(SlotInfo {
928 label: format!("{}[{}]", storage.label, slot_index),
929 slot_type: StorageTypeInfo {
930 label: storage_type.label.clone(),
931 dyn_sol_type: DynSolType::FixedBytes(32),
933 },
934 offset: 0,
935 slot: slot_str.to_string(),
936 members: None,
937 decoded: None,
938 keys: None,
939 });
940 }
941 }
942
943 None
944 }
945
946 fn resolve_mapping_type(&self, type_ref: &str) -> Option<(Vec<String>, String, String)> {
947 let storage_type = self.storage_layout.types.get(type_ref)?;
948
949 if storage_type.encoding != ENCODING_MAPPING {
950 return Some((vec![], storage_type.label.clone(), storage_type.label.clone()));
952 }
953
954 let key_type_ref = storage_type.key.as_ref()?;
956 let value_type_ref = storage_type.value.as_ref()?;
957
958 let key_type = self.storage_layout.types.get(key_type_ref)?;
960 let mut key_types = vec![key_type.label.clone()];
961
962 if let Some(value_storage_type) = self.storage_layout.types.get(value_type_ref) {
964 if value_storage_type.encoding == ENCODING_MAPPING {
965 let (nested_keys, final_value, _) = self.resolve_mapping_type(value_type_ref)?;
967 key_types.extend(nested_keys);
968 return Some((key_types, final_value, storage_type.label.clone()));
969 }
970 return Some((key_types, value_storage_type.label.clone(), storage_type.label.clone()));
972 }
973
974 None
975 }
976}
977
978fn get_array_base_indices(dyn_type: &DynSolType) -> String {
980 match dyn_type {
981 DynSolType::FixedArray(inner, _) => {
982 if let DynSolType::FixedArray(_, _) = inner.as_ref() {
983 format!("[0]{}", get_array_base_indices(inner))
985 } else {
986 "[0]".to_string()
988 }
989 }
990 _ => String::new(),
991 }
992}
993
994pub fn is_struct(s: &str) -> bool {
996 s.starts_with("struct ")
997}