Repository: lambdaclass/cairo-vm (language: rust)

Issue:
Bug in cairo1-run `--print_output`: Only First Field of Struct Retrieved
**Describe the bug**
There is a bug when trying to output a Felt252Dict of structs. It only fetches values in the first field of the struct.

**To Reproduce**
Here is the program I tested:
```cairo
use core::nullable::{nullable_from_box, match_nullable, FromNullableResult};


#[derive(Drop, Copy)]
struct FP16x16 {
    mag: u32,
    sign: bool
}

fn main() -> Felt252Dict<Nullable<FP16x16>> {
    // Create the dictionary
    let mut d: Felt252Dict<Nullable<FP16x16>> = Default::default();

    // Insert it as a `Span`
    d.insert(0, nullable_from_box(BoxTrait::new(FP16x16 { mag: 1, sign: true })));
    d.insert(1, nullable_from_box(BoxTrait::new(FP16x16 { mag: 1, sign: false })));
    d.insert(2, nullable_from_box(BoxTrait::new(FP16x16 { mag: 1, sign: true })));

    d
}
```

Execution command: `cargo run programs/dict_fp.cairo --layout all_cairo --print_output`

**Expected behavior**
It returned: `Program Output : {0:1 1:2 2:3}`,
instead of `Program Output : {0: 1 1 1: 2 0 2: 3 1}`

**What version/commit are you on?**
commit `94a3d3b`


Discussion / hints:

Fix patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 5dd8f750e4..8e320723e7 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,12 @@
 
 #### Upcoming Changes
 
+* feat(BREAKING): Use return type info from sierra when serializing return values in cairo1-run crate [#1665](https://github.com/lambdaclass/cairo-vm/pull/1665)
+  * Removed public function `serialize_output`.
+  * Add field `serialize_output` to `Cairo1RunConfig`.
+  * Function `cairo_run_program` now returns an extra `Option<String>` value with the serialized output if `serialize_output` is enabled in the config.
+  * Output serialization improved as it now uses the sierra program data to identify return value's types.
+
 * feat: Create hyper_threading crate to benchmark the `cairo-vm` in a hyper-threaded environment [#1679](https://github.com/lambdaclass/cairo-vm/pull/1679)
 
 * feat: add a `--tracer` option which hosts a web server that shows the line by line execution of cairo code along with memory registers [#1265](https://github.com/lambdaclass/cairo-vm/pull/1265)
diff --git a/cairo1-run/Cargo.toml b/cairo1-run/Cargo.toml
index 391774f86a..a831387a82 100644
--- a/cairo1-run/Cargo.toml
+++ b/cairo1-run/Cargo.toml
@@ -26,6 +26,7 @@ bincode.workspace = true
 assert_matches = "1.5.0"
 rstest = "0.17.0"
 mimalloc = { version = "0.1.37", default-features = false, optional = true }
+num-traits = { version = "0.2", default-features = false }
 
 [features]
 default = ["with_mimalloc"]
diff --git a/cairo1-run/src/cairo_run.rs b/cairo1-run/src/cairo_run.rs
index 8376727918..fec7569fad 100644
--- a/cairo1-run/src/cairo_run.rs
+++ b/cairo1-run/src/cairo_run.rs
@@ -13,11 +13,11 @@ use cairo_lang_sierra::{
         ConcreteType, NamedType,
     },
     ids::ConcreteTypeId,
-    program::{Function, Program as SierraProgram},
+    program::{Function, GenericArg, Program as SierraProgram},
     program_registry::ProgramRegistry,
 };
 use cairo_lang_sierra_ap_change::calc_ap_changes;
-use cairo_lang_sierra_gas::gas_info::GasInfo;
+use cairo_lang_sierra_gas::{gas_info::GasInfo, objects::CostInfoProvider};
 use cairo_lang_sierra_to_casm::{
     compiler::CairoProgram,
     metadata::{calc_metadata, Metadata, MetadataComputationConfig, MetadataError},
@@ -26,6 +26,7 @@ use cairo_lang_sierra_type_size::get_type_size_map;
 use cairo_lang_utils::unordered_hash_map::UnorderedHashMap;
 use cairo_vm::{
     hint_processor::cairo_1_hint_processor::hint_processor::Cairo1HintProcessor,
+    math_utils::signed_felt,
     serde::deserialize_program::{
         ApTracking, BuiltinName, FlowTrackingData, HintParams, ReferenceManager,
     },
@@ -43,14 +44,17 @@ use cairo_vm::{
     },
     Felt252,
 };
-use itertools::chain;
-use std::collections::HashMap;
+use itertools::{chain, Itertools};
+use num_traits::{cast::ToPrimitive, Zero};
+use std::{collections::HashMap, iter::Peekable, slice::Iter};
 
 use crate::{Error, FuncArg};
 
 #[derive(Debug)]
 pub struct Cairo1RunConfig<'a> {
     pub args: &'a [FuncArg],
+    // Serializes program output into a user-friendly format
+    pub serialize_output: bool,
     pub trace_enabled: bool,
     pub relocate_mem: bool,
     pub layout: &'a str,
@@ -66,6 +70,7 @@ impl Default for Cairo1RunConfig<'_> {
     fn default() -> Self {
         Self {
             args: Default::default(),
+            serialize_output: false,
             trace_enabled: false,
             relocate_mem: false,
             layout: "plain",
@@ -77,11 +82,19 @@ impl Default for Cairo1RunConfig<'_> {
 }
 
 // Runs a Cairo 1 program
-// Returns the runner & VM after execution + the return values
+// Returns the runner & VM after execution + the return values + the serialized return values (if serialize_output is enabled)
 pub fn cairo_run_program(
     sierra_program: &SierraProgram,
     cairo_run_config: Cairo1RunConfig,
-) -> Result<(CairoRunner, VirtualMachine, Vec<MaybeRelocatable>), Error> {
+) -> Result<
+    (
+        CairoRunner,
+        VirtualMachine,
+        Vec<MaybeRelocatable>,
+        Option<String>,
+    ),
+    Error,
+> {
     let metadata = create_metadata(sierra_program, Some(Default::default()))?;
     let sierra_program_registry = ProgramRegistry::<CoreType, CoreLibfunc>::new(sierra_program)?;
     let type_sizes =
@@ -211,6 +224,18 @@ pub fn cairo_run_program(
     // Fetch return values
     let return_values = fetch_return_values(return_type_size, return_type_id, &vm)?;
 
+    let serialized_output = if cairo_run_config.serialize_output {
+        Some(serialize_output(
+            &return_values,
+            &mut vm,
+            return_type_id,
+            &sierra_program_registry,
+            &type_sizes,
+        ))
+    } else {
+        None
+    };
+
     // Set stop pointers for builtins so we can obtain the air public input
     if cairo_run_config.finalize_builtins {
         finalize_builtins(
@@ -233,7 +258,7 @@ pub fn cairo_run_program(
 
     runner.relocate(&mut vm, true)?;
 
-    Ok((runner, vm, return_values))
+    Ok((runner, vm, return_values, serialized_output))
 }
 
 fn additional_initialization(vm: &mut VirtualMachine, data_len: usize) -> Result<(), Error> {
@@ -726,6 +751,371 @@ fn finalize_builtins(
     Ok(())
 }
 
+fn serialize_output(
+    return_values: &[MaybeRelocatable],
+    vm: &mut VirtualMachine,
+    return_type_id: &ConcreteTypeId,
+    sierra_program_registry: &ProgramRegistry<CoreType, CoreLibfunc>,
+    type_sizes: &UnorderedHashMap<ConcreteTypeId, i16>,
+) -> String {
+    let mut output_string = String::new();
+    let mut return_values_iter: Peekable<Iter<MaybeRelocatable>> = return_values.iter().peekable();
+    serialize_output_inner(
+        &mut return_values_iter,
+        &mut output_string,
+        vm,
+        return_type_id,
+        sierra_program_registry,
+        type_sizes,
+    );
+    output_string
+}
+
+fn serialize_output_inner(
+    return_values_iter: &mut Peekable<Iter<MaybeRelocatable>>,
+    output_string: &mut String,
+    vm: &mut VirtualMachine,
+    return_type_id: &ConcreteTypeId,
+    sierra_program_registry: &ProgramRegistry<CoreType, CoreLibfunc>,
+    type_sizes: &UnorderedHashMap<ConcreteTypeId, i16>,
+) {
+    match sierra_program_registry.get_type(return_type_id).unwrap() {
+        cairo_lang_sierra::extensions::core::CoreTypeConcrete::Array(info) => {
+            // Fetch array from memory
+            let array_start = return_values_iter
+                .next()
+                .expect("Missing return value")
+                .get_relocatable()
+                .expect("Array start_ptr not Relocatable");
+            // Arrays can come in two formats: either [start_ptr, end_ptr] or [end_ptr], with the start_ptr being implicit (base of the end_ptr's segment)
+            let (array_start, array_size ) = match return_values_iter.peek().and_then(|mr| mr.get_relocatable()) {
+                Some(array_end) if array_end.segment_index == array_start.segment_index && array_end.offset >= array_start.offset  => {
+                    // Pop the value we just peeked
+                    return_values_iter.next();
+                    (array_start, (array_end - array_start).unwrap())
+                }
+                _ => ((array_start.segment_index, 0).into(), array_start.offset),
+            };
+            let array_data = vm.get_continuous_range(array_start, array_size).unwrap();
+            let mut array_data_iter = array_data.iter().peekable();
+            let array_elem_id = &info.ty;
+            // Serialize array data
+            maybe_add_whitespace(output_string);
+            output_string.push('[');
+            while array_data_iter.peek().is_some() {
+                serialize_output_inner(
+                    &mut array_data_iter,
+                    output_string,
+                    vm,
+                    array_elem_id,
+                    sierra_program_registry,
+                    type_sizes,
+                )
+            }
+            output_string.push(']');
+        }
+        cairo_lang_sierra::extensions::core::CoreTypeConcrete::Box(info) => {
+            // As this represents a pointer, we need to extract it's values
+            let ptr = return_values_iter
+                .next()
+                .expect("Missing return value")
+                .get_relocatable()
+                .expect("Box Pointer is not Relocatable");
+            let type_size = type_sizes.type_size(&info.ty);
+            let data = vm
+                .get_continuous_range(ptr, type_size)
+                .expect("Failed to extract value from nullable ptr");
+            let mut data_iter = data.iter().peekable();
+            serialize_output_inner(
+                &mut data_iter,
+                output_string,
+                vm,
+                &info.ty,
+                sierra_program_registry,
+                type_sizes,
+            )
+        }
+        cairo_lang_sierra::extensions::core::CoreTypeConcrete::Const(_) => {
+            unimplemented!("Not supported in the current version")
+        },
+        cairo_lang_sierra::extensions::core::CoreTypeConcrete::Felt252(_)
+        // Only unsigned integer values implement Into<Bytes31>
+        | cairo_lang_sierra::extensions::core::CoreTypeConcrete::Bytes31(_)
+        | cairo_lang_sierra::extensions::core::CoreTypeConcrete::Uint8(_)
+        | cairo_lang_sierra::extensions::core::CoreTypeConcrete::Uint16(_)
+        | cairo_lang_sierra::extensions::core::CoreTypeConcrete::Uint32(_)
+        | cairo_lang_sierra::extensions::core::CoreTypeConcrete::Uint64(_)
+        | cairo_lang_sierra::extensions::core::CoreTypeConcrete::Uint128(_) => {
+            maybe_add_whitespace(output_string);
+            let val = return_values_iter
+                .next()
+                .expect("Missing return value")
+                .get_int()
+                .expect("Value is not an integer");
+            output_string.push_str(&val.to_string());
+        }
+        cairo_lang_sierra::extensions::core::CoreTypeConcrete::Sint8(_)
+        | cairo_lang_sierra::extensions::core::CoreTypeConcrete::Sint16(_)
+        | cairo_lang_sierra::extensions::core::CoreTypeConcrete::Sint32(_)
+        | cairo_lang_sierra::extensions::core::CoreTypeConcrete::Sint64(_)
+        | cairo_lang_sierra::extensions::core::CoreTypeConcrete::Sint128(_) => {
+            maybe_add_whitespace(output_string);
+            let val = return_values_iter
+                .next()
+                .expect("Missing return value")
+                .get_int()
+                .expect("Value is not an integer");
+            output_string.push_str(&signed_felt(val).to_string());
+        }
+        cairo_lang_sierra::extensions::core::CoreTypeConcrete::NonZero(info) => {
+            serialize_output_inner(
+                return_values_iter,
+                output_string,
+                vm,
+                &info.ty,
+                sierra_program_registry,
+                type_sizes,
+            )
+        }
+        cairo_lang_sierra::extensions::core::CoreTypeConcrete::Nullable(info) => {
+            // As this represents a pointer, we need to extract it's values
+            let ptr = match return_values_iter.next().expect("Missing return value") {
+                MaybeRelocatable::RelocatableValue(ptr) => *ptr,
+                MaybeRelocatable::Int(felt) if felt.is_zero() => {
+                    // Nullable is Null
+                    maybe_add_whitespace(output_string);
+                    output_string.push_str("null");
+                    return;
+                }
+                _ => panic!("Invalid Nullable"),
+            };
+            let type_size = type_sizes.type_size(&info.ty);
+            let data = vm
+                .get_continuous_range(ptr, type_size)
+                .expect("Failed to extract value from nullable ptr");
+            let mut data_iter = data.iter().peekable();
+            serialize_output_inner(
+                &mut data_iter,
+                output_string,
+                vm,
+                &info.ty,
+                sierra_program_registry,
+                type_sizes,
+            )
+        }
+        cairo_lang_sierra::extensions::core::CoreTypeConcrete::Enum(info) => {
+            // First we check if it is a Panic enum, as we already handled panics when fetching return values,
+            // we can ignore them and move on to the non-panic variant
+            if let GenericArg::UserType(user_type) = &info.info.long_id.generic_args[0] {
+                if user_type
+                    .debug_name
+                    .as_ref()
+                    .is_some_and(|n| n.starts_with("core::panics::PanicResult"))
+                {
+                    return serialize_output_inner(
+                        return_values_iter,
+                        output_string,
+                        vm,
+                        &info.variants[0],
+                        sierra_program_registry,
+                        type_sizes,
+                    );
+                }
+            }
+            let num_variants = &info.variants.len();
+            let casm_variant_idx: usize = return_values_iter
+                .next()
+                .expect("Missing return value")
+                .get_int()
+                .expect("Enum tag is not integer")
+                .to_usize()
+                .expect("Invalid enum tag");
+            // Convert casm variant idx to sierra variant idx
+            let variant_idx = if *num_variants > 2 {
+                num_variants - 1 - (casm_variant_idx >> 1)
+            } else {
+                casm_variant_idx
+            };
+            let variant_type_id = &info.variants[variant_idx];
+
+            // Handle core::bool separately
+            if let GenericArg::UserType(user_type) = &info.info.long_id.generic_args[0] {
+                if user_type
+                    .debug_name
+                    .as_ref()
+                    .is_some_and(|n| n == "core::bool")
+                {
+                    // Sanity checks
+                    assert!(
+                        *num_variants == 2
+                            && variant_idx < 2
+                            && type_sizes
+                                .get(&info.variants[0])
+                                .is_some_and(|size| size.is_zero())
+                            && type_sizes
+                                .get(&info.variants[1])
+                                .is_some_and(|size| size.is_zero()),
+                        "Malformed bool enum"
+                    );
+
+                    let boolean_string = match variant_idx {
+                        0 => "false",
+                        _ => "true",
+                    };
+                    maybe_add_whitespace(output_string);
+                    output_string.push_str(boolean_string);
+                    return;
+                }
+            }
+            // TODO: Something similar to the bool handling could be done for unit enum variants if we could get the type info with the variant names
+
+            // Space is always allocated for the largest enum member, padding with zeros in front for the smaller variants
+            let mut max_variant_size = 0;
+            for variant in &info.variants {
+                let variant_size = type_sizes.get(variant).unwrap();
+                max_variant_size = std::cmp::max(max_variant_size, *variant_size)
+            }
+            for _ in 0..max_variant_size - type_sizes.get(variant_type_id).unwrap() {
+                // Remove padding
+                assert_eq!(
+                    return_values_iter.next(),
+                    Some(&MaybeRelocatable::from(0)),
+                    "Malformed enum"
+                );
+            }
+            serialize_output_inner(
+                return_values_iter,
+                output_string,
+                vm,
+                variant_type_id,
+                sierra_program_registry,
+                type_sizes,
+            )
+        }
+        cairo_lang_sierra::extensions::core::CoreTypeConcrete::Struct(info) => {
+            for member_type_id in &info.members {
+                serialize_output_inner(
+                    return_values_iter,
+                    output_string,
+                    vm,
+                    member_type_id,
+                    sierra_program_registry,
+                    type_sizes,
+                )
+            }
+        }
+        cairo_lang_sierra::extensions::core::CoreTypeConcrete::Felt252Dict(info) => {
+            // Process Dictionary
+            let dict_ptr = return_values_iter
+                .next()
+                .expect("Missing return val")
+                .get_relocatable()
+                .expect("Dict Ptr not Relocatable");
+            if !(dict_ptr.offset
+                == vm
+                    .get_segment_size(dict_ptr.segment_index as usize)
+                    .unwrap_or_default()
+                && dict_ptr.offset % 3 == 0)
+            {
+                panic!("Return value is not a valid Felt252Dict")
+            }
+            // Fetch dictionary values type id
+            let value_type_id = &info.ty;
+            // Fetch the dictionary's memory
+            let dict_mem = vm
+                .get_continuous_range((dict_ptr.segment_index, 0).into(), dict_ptr.offset)
+                .expect("Malformed dictionary memory");
+            // Serialize the dictionary
+            output_string.push('{');
+            // The dictionary's memory is made up of (key, prev_value, next_value) tuples
+            // The prev value is not relevant to the user so we can skip over it for calrity
+            for (key, _, value) in dict_mem.iter().tuples() {
+                maybe_add_whitespace(output_string);
+                // Serialize the key wich should always be a Felt value
+                output_string.push_str(&key.to_string());
+                output_string.push(':');
+                // Serialize the value
+                // We create a peekable array here in order to use the serialize_output_inner as the value could be a span
+                let value_vec = vec![value.clone()];
+                let mut value_iter: Peekable<Iter<MaybeRelocatable>> = value_vec.iter().peekable();
+                serialize_output_inner(
+                    &mut value_iter,
+                    output_string,
+                    vm,
+                    value_type_id,
+                    sierra_program_registry,
+                    type_sizes,
+                );
+            }
+            output_string.push('}');
+        }
+        cairo_lang_sierra::extensions::core::CoreTypeConcrete::SquashedFelt252Dict(info) => {
+            // Process Dictionary
+            let dict_start = return_values_iter
+                .next()
+                .expect("Missing return val")
+                .get_relocatable()
+                .expect("Squashed dict_start ptr not Relocatable");
+            let dict_end = return_values_iter
+                .next()
+                .expect("Missing return val")
+                .get_relocatable()
+                .expect("Squashed dict_end ptr not Relocatable");
+            let dict_size = (dict_end - dict_start).unwrap();
+            if dict_size % 3 != 0 {
+                panic!("Return value is not a valid SquashedFelt252Dict")
+            }
+            // Fetch dictionary values type id
+            let value_type_id = &info.ty;
+            // Fetch the dictionary's memory
+            let dict_mem = vm
+                .get_continuous_range(dict_start, dict_size)
+                .expect("Malformed squashed dictionary memory");
+            // Serialize the dictionary
+            output_string.push('{');
+            // The dictionary's memory is made up of (key, prev_value, next_value) tuples
+            // The prev value is not relevant to the user so we can skip over it for calrity
+            for (key, _, value) in dict_mem.iter().tuples() {
+                maybe_add_whitespace(output_string);
+                // Serialize the key wich should always be a Felt value
+                output_string.push_str(&key.to_string());
+                output_string.push(':');
+                // Serialize the value
+                // We create a peekable array here in order to use the serialize_output_inner as the value could be a span
+                let value_vec = vec![value.clone()];
+                let mut value_iter: Peekable<Iter<MaybeRelocatable>> = value_vec.iter().peekable();
+                serialize_output_inner(
+                    &mut value_iter,
+                    output_string,
+                    vm,
+                    value_type_id,
+                    sierra_program_registry,
+                    type_sizes,
+                );
+            }
+            output_string.push('}');
+        }
+        cairo_lang_sierra::extensions::core::CoreTypeConcrete::Span(_) => unimplemented!("Span types get resolved to Array in the current version"),
+        cairo_lang_sierra::extensions::core::CoreTypeConcrete::Snapshot(info) => {
+            serialize_output_inner(
+                return_values_iter,
+                output_string,
+                vm,
+                &info.ty,
+                sierra_program_registry,
+                type_sizes,
+            )
+        }
+        _ => panic!("Unexpected return type")
+    }
+}
+
+fn maybe_add_whitespace(string: &mut String) {
+    if !string.is_empty() && !string.ends_with('[') && !string.ends_with('{') {
+        string.push(' ');
+    }
+}
+
 #[cfg(test)]
 mod tests {
     use std::path::Path;
@@ -791,7 +1181,7 @@ mod tests {
             ..Default::default()
         };
         // Run program
-        let (runner, vm, return_values) =
+        let (runner, vm, return_values, _) =
             cairo_run_program(&sierra_program, cairo_run_config).unwrap();
         // When the return type is a PanicResult, we remove the panic wrapper when returning the ret values
         // And handle the panics returning an error, so we need to add it here
diff --git a/cairo1-run/src/main.rs b/cairo1-run/src/main.rs
index e9875babb7..e8aeabadb6 100644
--- a/cairo1-run/src/main.rs
+++ b/cairo1-run/src/main.rs
@@ -6,13 +6,10 @@ use cairo_run::Cairo1RunConfig;
 use cairo_vm::{
     air_public_input::PublicInputError,
     cairo_run::EncodeTraceError,
-    types::{errors::program_errors::ProgramError, relocatable::MaybeRelocatable},
-    vm::{
-        errors::{
-            memory_errors::MemoryError, runner_errors::RunnerError, trace_errors::TraceError,
-            vm_errors::VirtualMachineError,
-        },
-        vm_core::VirtualMachine,
+    types::errors::program_errors::ProgramError,
+    vm::errors::{
+        memory_errors::MemoryError, runner_errors::RunnerError, trace_errors::TraceError,
+        vm_errors::VirtualMachineError,
     },
     Felt252,
 };
@@ -20,9 +17,7 @@ use clap::{Parser, ValueHint};
 use itertools::Itertools;
 use std::{
     io::{self, Write},
-    iter::Peekable,
     path::PathBuf,
-    slice::Iter,
 };
 use thiserror::Error;
 
@@ -216,6 +211,7 @@ fn run(args: impl Iterator<Item = String>) -> Result<Option<String>, Error> {
 
     let cairo_run_config = Cairo1RunConfig {
         proof_mode: args.proof_mode,
+        serialize_output: args.print_output,
         relocate_mem: args.memory_file.is_some() || args.air_public_input.is_some(),
         layout: &args.layout,
         trace_enabled: args.trace_file.is_some() || args.air_public_input.is_some(),
@@ -232,15 +228,9 @@ fn run(args: impl Iterator<Item = String>) -> Result<Option<String>, Error> {
     let sierra_program = compile_cairo_project_at_path(&args.filename, compiler_config)
         .map_err(|err| Error::SierraCompilation(err.to_string()))?;
 
-    let (runner, vm, return_values) =
+    let (runner, vm, _, serialized_output) =
         cairo_run::cairo_run_program(&sierra_program, cairo_run_config)?;
 
-    let output_string = if args.print_output {
-        Some(serialize_output(&vm, &return_values))
-    } else {
-        None
-    };
-
     if let Some(file_path) = args.air_public_input {
         let json = runner.get_air_public_input(&vm)?.serialize_json()?;
         std::fs::write(file_path, json)?;
@@ -297,7 +287,7 @@ fn run(args: impl Iterator<Item = String>) -> Result<Option<String>, Error> {
         memory_writer.flush()?;
     }
 
-    Ok(output_string)
+    Ok(serialized_output)
 }
 
 fn main() -> Result<(), Error> {
@@ -331,116 +321,6 @@ fn main() -> Result<(), Error> {
     }
 }
 
-/// Serializes the return values in a user-friendly format
-/// Displays Arrays using brackets ([]) and Dictionaries using ({})
-/// Recursively dereferences referenced values (such as Span & Box)
-pub fn serialize_output(vm: &VirtualMachine, return_values: &[MaybeRelocatable]) -> String {
-    let mut output_string = String::new();
-    let mut return_values_iter: Peekable<Iter<MaybeRelocatable>> = return_values.iter().peekable();
-    serialize_output_inner(&mut return_values_iter, &mut output_string, vm);
-    fn serialize_output_inner(
-        iter: &mut Peekable<Iter<MaybeRelocatable>>,
-        output_string: &mut String,
-        vm: &VirtualMachine,
-    ) {
-        while let Some(val) = iter.next() {
-            if let MaybeRelocatable::RelocatableValue(x) = val {
-                // Check if the next value is a relocatable of the same index
-                if let Some(MaybeRelocatable::RelocatableValue(y)) = iter.peek() {
-                    // Check if the two relocatable values represent a valid array in memory
-                    if x.segment_index == y.segment_index && x.offset <= y.offset {
-                        // Fetch the y value from the iterator so we don't serialize it twice
-                        iter.next();
-                        // Fetch array
-                        maybe_add_whitespace(output_string);
-                        output_string.push('[');
-                        let array = vm.get_continuous_range(*x, y.offset - x.offset).unwrap();
-                        let mut array_iter: Peekable<Iter<MaybeRelocatable>> =
-                            array.iter().peekable();
-                        serialize_output_inner(&mut array_iter, output_string, vm);
-                        output_string.push(']');
-                        continue;
-                    }
-                }
-
-                // Check if the single relocatable value represents a span
-                // In this case, the reloacatable will point us to the (start, end) pair in memory
-                // For example, the relocatable value may be 14:0, with the segment 14 containing [13:0 13:4] which is a valid array
-                if let (Ok(x), Ok(y)) = (vm.get_relocatable(*x), vm.get_relocatable(x + 1)) {
-                    if x.segment_index == y.segment_index && y.offset >= x.offset {
-                        // Fetch array
-                        maybe_add_whitespace(output_string);
-                        output_string.push('[');
-                        let array = vm.get_continuous_range(x, y.offset - x.offset).unwrap();
-                        let mut array_iter: Peekable<Iter<MaybeRelocatable>> =
-                            array.iter().peekable();
-                        serialize_output_inner(&mut array_iter, output_string, vm);
-                        output_string.push(']');
-                        continue;
-                    }
-                }
-
-                // Check if the relocatable value represents a dictionary
-                // To do so we can check if the relocatable consists of the last dict_ptr, which should be a pointer to the next empty cell in the dictionary's segment
-                // We can check that the dict_ptr's offset is consistent with the length of the segment and that the segment is made up of tuples of three elements (key, prev_val, val)
-                if x.offset
-                    == vm
-                        .get_segment_size(x.segment_index as usize)
-                        .unwrap_or_default()
-                    && x.offset % 3 == 0
-                {
-                    // Fetch the dictionary's memory
-                    let dict_mem = vm
-                        .get_continuous_range((x.segment_index, 0).into(), x.offset)
-                        .expect("Malformed dictionary memory");
-                    // Serialize the dictionary
-                    output_string.push('{');
-                    // The dictionary's memory is made up of (key, prev_value, next_value) tuples
-                    // The prev value is not relevant to the user so we can skip over it for calrity
-                    for (key, _, value) in dict_mem.iter().tuples() {
-                        maybe_add_whitespace(output_string);
-                        // Serialize the key wich should always be a Felt value
-                        output_string.push_str(&key.to_string());
-                        output_string.push(':');
-                        // Serialize the value
-                        // We create a peekable array here in order to use the serialize_output_inner as the value could be a span
-                        let value_vec = vec![value.clone()];
-                        let mut value_iter: Peekable<Iter<MaybeRelocatable>> =
-                            value_vec.iter().peekable();
-                        serialize_output_inner(&mut value_iter, output_string, vm);
-                    }
-                    output_string.push('}');
-                    continue;
-                }
-
-                // Finally, if the relocatable is neither the start of an array, a span, or a dictionary, it should be a reference (Such as Box)
-                // In this case we show the referenced value (if it exists)
-                // As this reference can also hold a reference we use the serialize_output_inner function to handle it recursively
-                if let Some(val) = vm.get_maybe(x) {
-                    maybe_add_whitespace(output_string);
-                    let array = vec![val.clone()];
-                    let mut array_iter: Peekable<Iter<MaybeRelocatable>> = array.iter().peekable();
-                    serialize_output_inner(&mut array_iter, output_string, vm);
-                    continue;
-                }
-            }
-            maybe_add_whitespace(output_string);
-            output_string.push_str(&val.to_string());
-        }
-    }
-
-    fn maybe_add_whitespace(string: &mut String) {
-        if !string.is_empty()
-            && !string.ends_with('[')
-            && !string.ends_with(':')
-            && !string.ends_with('{')
-        {
-            string.push(' ');
-        }
-    }
-    output_string
-}
-
 #[cfg(test)]
 mod tests {
     use super::*;
@@ -492,7 +372,7 @@ mod tests {
     #[case(["cairo1-run", "../cairo_programs/cairo-1-programs/hello.cairo", "--print_output", "--trace_file", "/dev/null", "--memory_file", "/dev/null", "--layout", "all_cairo", "--proof_mode", "--air_public_input", "/dev/null", "--air_private_input", "/dev/null"].as_slice())]
     fn test_run_hello_ok(#[case] args: &[&str]) {
         let args = args.iter().cloned().map(String::from);
-        assert_matches!(run(args), Ok(Some(res)) if res == "1 1234");
+        assert_matches!(run(args), Ok(Some(res)) if res == "1234");
     }
 
     #[rstest]
@@ -556,7 +436,7 @@ mod tests {
     #[case(["cairo1-run", "../cairo_programs/cairo-1-programs/simple.cairo", "--print_output", "--trace_file", "/dev/null", "--memory_file", "/dev/null", "--layout", "all_cairo", "--proof_mode", "--air_public_input", "/dev/null", "--air_private_input", "/dev/null"].as_slice())]
     fn test_run_simple_ok(#[case] args: &[&str]) {
         let args = args.iter().cloned().map(String::from);
-        assert_matches!(run(args), Ok(Some(res)) if res == "1");
+        assert_matches!(run(args), Ok(Some(res)) if res == "true");
     }
 
     #[rstest]
@@ -644,7 +524,7 @@ mod tests {
     #[case(["cairo1-run", "../cairo_programs/cairo-1-programs/felt_dict.cairo", "--print_output", "--trace_file", "/dev/null", "--memory_file", "/dev/null", "--layout", "all_cairo", "--proof_mode", "--air_public_input", "/dev/null", "--air_private_input", "/dev/null"].as_slice())]
     fn test_run_felt_dict(#[case] args: &[&str]) {
         let args = args.iter().cloned().map(String::from);
-        let expected_output = "{66675:[8 9 10 11] 66676:[1 2 3]}";
+        let expected_output = "{66675: [8 9 10 11] 66676: [1 2 3]}";
         assert_matches!(run(args), Ok(Some(res)) if res == expected_output);
     }
 
@@ -671,7 +551,52 @@ mod tests {
     #[case(["cairo1-run", "../cairo_programs/cairo-1-programs/nullable_box_vec.cairo", "--print_output", "--trace_file", "/dev/null", "--memory_file", "/dev/null", "--layout", "all_cairo", "--proof_mode", "--air_public_input", "/dev/null", "--air_private_input", "/dev/null"].as_slice())]
     fn test_run_nullable_box_vec(#[case] args: &[&str]) {
         let args = args.iter().cloned().map(String::from);
-        let expected_output = "{0:10 1:20 2:30} 3";
+        let expected_output = "{0: 10 1: 20 2: 30} 3";
+        assert_matches!(run(args), Ok(Some(res)) if res == expected_output);
+    }
+
+    #[rstest]
+    #[case(["cairo1-run", "../cairo_programs/cairo-1-programs/dict_with_struct.cairo", "--print_output", "--trace_file", "/dev/null", "--memory_file", "/dev/null", "--layout", "all_cairo", "--cairo_pie_output", "/dev/null"].as_slice())]
+    #[case(["cairo1-run", "../cairo_programs/cairo-1-programs/dict_with_struct.cairo", "--print_output", "--trace_file", "/dev/null", "--memory_file", "/dev/null", "--layout", "all_cairo", "--proof_mode", "--air_public_input", "/dev/null", "--air_private_input", "/dev/null"].as_slice())]
+    fn test_run_dict_with_struct(#[case] args: &[&str]) {
+        let args = args.iter().cloned().map(String::from);
+        let expected_output = "{0: 1 true 1: 1 false 2: 1 true}";
+        assert_matches!(run(args), Ok(Some(res)) if res == expected_output);
+    }
+
+    #[rstest]
+    #[case(["cairo1-run", "../cairo_programs/cairo-1-programs/felt_dict_squash.cairo", "--print_output", "--trace_file", "/dev/null", "--memory_file", "/dev/null", "--layout", "all_cairo", "--cairo_pie_output", "/dev/null"].as_slice())]
+    #[case(["cairo1-run", "../cairo_programs/cairo-1-programs/felt_dict_squash.cairo", "--print_output", "--trace_file", "/dev/null", "--memory_file", "/dev/null", "--layout", "all_cairo", "--proof_mode", "--air_public_input", "/dev/null", "--air_private_input", "/dev/null"].as_slice())]
+    fn test_run_felt_dict_squash(#[case] args: &[&str]) {
+        let args = args.iter().cloned().map(String::from);
+        let expected_output = "{66675: [4 5 6] 66676: [1 2 3]}";
+        assert_matches!(run(args), Ok(Some(res)) if res == expected_output);
+    }
+
+    #[rstest]
+    #[case(["cairo1-run", "../cairo_programs/cairo-1-programs/null_ret.cairo", "--print_output", "--trace_file", "/dev/null", "--memory_file", "/dev/null", "--layout", "all_cairo", "--cairo_pie_output", "/dev/null"].as_slice())]
+    #[case(["cairo1-run", "../cairo_programs/cairo-1-programs/null_ret.cairo", "--print_output", "--trace_file", "/dev/null", "--memory_file", "/dev/null", "--layout", "all_cairo", "--proof_mode", "--air_public_input", "/dev/null", "--air_private_input", "/dev/null"].as_slice())]
+    fn test_run_null_ret(#[case] args: &[&str]) {
+        let args = args.iter().cloned().map(String::from);
+        let expected_output = "null";
+        assert_matches!(run(args), Ok(Some(res)) if res == expected_output);
+    }
+
+    #[rstest]
+    #[case(["cairo1-run", "../cairo_programs/cairo-1-programs/bytes31_ret.cairo", "--print_output", "--trace_file", "/dev/null", "--memory_file", "/dev/null", "--layout", "all_cairo", "--cairo_pie_output", "/dev/null"].as_slice())]
+    #[case(["cairo1-run", "../cairo_programs/cairo-1-programs/bytes31_ret.cairo", "--print_output", "--trace_file", "/dev/null", "--memory_file", "/dev/null", "--layout", "all_cairo", "--proof_mode", "--air_public_input", "/dev/null", "--air_private_input", "/dev/null"].as_slice())]
+    fn test_run_bytes31_ret(#[case] args: &[&str]) {
+        let args = args.iter().cloned().map(String::from);
+        let expected_output = "123";
+        assert_matches!(run(args), Ok(Some(res)) if res == expected_output);
+    }
+
+    #[rstest]
+    #[case(["cairo1-run", "../cairo_programs/cairo-1-programs/tensor_new.cairo", "--print_output", "--trace_file", "/dev/null", "--memory_file", "/dev/null", "--layout", "all_cairo", "--cairo_pie_output", "/dev/null"].as_slice())]
+    #[case(["cairo1-run", "../cairo_programs/cairo-1-programs/tensor_new.cairo", "--print_output", "--trace_file", "/dev/null", "--memory_file", "/dev/null", "--layout", "all_cairo", "--proof_mode", "--air_public_input", "/dev/null", "--air_private_input", "/dev/null"].as_slice())]
+    fn test_run_tensor_new(#[case] args: &[&str]) {
+        let args = args.iter().cloned().map(String::from);
+        let expected_output = "[1 2] [1 false 1 true]";
         assert_matches!(run(args), Ok(Some(res)) if res == expected_output);
     }
 }
diff --git a/cairo_programs/cairo-1-programs/bytes31_ret.cairo b/cairo_programs/cairo-1-programs/bytes31_ret.cairo
new file mode 100644
index 0000000000..3a67f9cbf5
--- /dev/null
+++ b/cairo_programs/cairo-1-programs/bytes31_ret.cairo
@@ -0,0 +1,5 @@
+fn main() -> bytes31 {
+    let a: u128 = 123;
+    let b: bytes31 = a.into();
+    b
+}
diff --git a/cairo_programs/cairo-1-programs/dict_with_struct.cairo b/cairo_programs/cairo-1-programs/dict_with_struct.cairo
new file mode 100644
index 0000000000..e24df874c2
--- /dev/null
+++ b/cairo_programs/cairo-1-programs/dict_with_struct.cairo
@@ -0,0 +1,24 @@
+use core::nullable::{nullable_from_box, match_nullable, FromNullableResult};
+
+
+#[derive(Drop, Copy)]
+struct FP16x16 {
+    mag: u32,
+    sign: bool
+}
+
+fn main() -> Felt252Dict<Nullable<FP16x16>> {
+    // Create the dictionary
+    let mut d: Felt252Dict<Nullable<FP16x16>> = Default::default();
+
+    let box_a = BoxTrait::new(FP16x16 { mag: 1, sign: false });
+    let box_b = BoxTrait::new(FP16x16 { mag: 1, sign: true });
+    let box_c = BoxTrait::new(FP16x16 { mag: 1, sign: true });
+
+    // Insert it as a `Span`
+    d.insert(0, nullable_from_box(box_c));
+    d.insert(1, nullable_from_box(box_a));
+    d.insert(2, nullable_from_box(box_b));
+
+    d
+}
diff --git a/cairo_programs/cairo-1-programs/felt_dict_squash.cairo b/cairo_programs/cairo-1-programs/felt_dict_squash.cairo
new file mode 100644
index 0000000000..7f1e284e32
--- /dev/null
+++ b/cairo_programs/cairo-1-programs/felt_dict_squash.cairo
@@ -0,0 +1,18 @@
+use core::nullable::{nullable_from_box, match_nullable, FromNullableResult};
+use core::dict::Felt252DictEntry;
+
+fn main() -> SquashedFelt252Dict<Nullable<Span<felt252>>> {
+    // Create the dictionary
+    let mut d: Felt252Dict<Nullable<Span<felt252>>> = Default::default();
+
+    // Create the array to insert
+    let a = array![8, 9, 10, 11];
+    let b = array![1, 2, 3];
+    let c = array![4, 5, 6];
+
+    // Insert it as a `Span`
+    d.insert(66675, nullable_from_box(BoxTrait::new(a.span())));
+    d.insert(66676, nullable_from_box(BoxTrait::new(b.span())));
+    d.insert(66675, nullable_from_box(BoxTrait::new(c.span())));
+    d.squash()
+}
diff --git a/cairo_programs/cairo-1-programs/null_ret.cairo b/cairo_programs/cairo-1-programs/null_ret.cairo
new file mode 100644
index 0000000000..85769a8e15
--- /dev/null
+++ b/cairo_programs/cairo-1-programs/null_ret.cairo
@@ -0,0 +1,3 @@
+fn main() -> Nullable<u32> {
+   null()
+}
diff --git a/cairo_programs/cairo-1-programs/tensor_new.cairo b/cairo_programs/cairo-1-programs/tensor_new.cairo
new file mode 100644
index 0000000000..2af717beed
--- /dev/null
+++ b/cairo_programs/cairo-1-programs/tensor_new.cairo
@@ -0,0 +1,65 @@
+// FP16x16
+#[derive(Serde, Copy, Drop)]
+struct FP16x16 {
+    mag: u32,
+    sign: bool
+}
+
+trait FixedTrait<T, MAG> {
+    fn new(mag: MAG, sign: bool) -> T;
+}
+
+impl FP16x16Impl of FixedTrait<FP16x16, u32> {
+    fn new(mag: u32, sign: bool) -> FP16x16 {
+        FP16x16 { mag: mag, sign: sign }
+    }
+}
+
+//Tensor
+#[derive(Copy, Drop)]
+struct Tensor<T> {
+    shape: Span<usize>,
+    data: Span<T>,
+}
+
+trait TensorTrait<T> {
+    fn new(shape: Span<usize>, data: Span<T>) -> Tensor<T>;
+}
+
+impl FP16x16Tensor of TensorTrait<FP16x16> {
+    fn new(shape: Span<usize>, data: Span<FP16x16>) -> Tensor<FP16x16> {
+        new_tensor(shape, data)
+    }
+}
+
+fn new_tensor<T>(shape: Span<usize>, data: Span<T>) -> Tensor<T> {
+    check_shape::<T>(shape, data);
+    Tensor::<T> { shape, data }
+}
+
+fn check_shape<T>(shape: Span<usize>, data: Span<T>) {
+    assert(len_from_shape(shape) == data.len(), 'wrong tensor shape');
+}
+
+fn len_from_shape(mut shape: Span<usize>) -> usize {
+    let mut result: usize = 1;
+
+    loop {
+        match shape.pop_front() {
+            Option::Some(item) => { result *= *item; },
+            Option::None => { break; }
+        };
+    };
+
+    result
+}
+
+fn main() -> Tensor<FP16x16> {
+     TensorTrait::new(
+         array![1, 2].span(),
+         array![
+           FixedTrait::new(1, false), 
+           FixedTrait::new(1, true)
+         ].span()
+    )
+}


Test patch: