Ver Fonte

feat(soroban): Support structs (#1843)

This PR brings support to user defined structs in Soroban's Solidity.

Here are some points to note:

- In-memory structs were naturally supported by integrating a
sorobanVm-friendly bump allocator that was introduced in #1838

- For storing structs in storage, we do as follows:
We encode struct fields as Soroban Vals, and store each one separately
in storage where the key of each element is SorobanVec[storage_slot,
field_index].

When we retrieve a whole struct, i.e `StructDef memory my_struct =
my_structs_map[my_struct]`, we simply loop over all fields, fetch them
from storage, and create an in memory instance.

That is why we strongly advise to access a single struct field at a time
instead of fetching it all from storage at once. This a complete
example:

```
contract timelock {
    
    mapping(address => TimeLock) timelocks;
    
    struct TimeLock {
        uint64 release_time;
        address beneficiary;
        uint64 amount;
    }

   

    function create_timelock(
        uint64 release_time,
        address beneficiary,
        uint64 amount
    ) public returns (uint64) {

        TimeLock memory tl = TimeLock({
            release_time: release_time,
            beneficiary: beneficiary,
            amount: amount
        });

        timelocks[beneficiary] = tl;

        return tl.amount;
    }

    function get_timelock_amount_cheap_op(address beneficiary) public view returns (uint64) {
      return timelocks[beneficiary].amount;
    }

    function get_timelock_amount_expensive_op(address beneficiary) public view returns (uint64) {
      TimeLock memory tl = timelocks[beneficiary];
      return tl.amount;
    }
}
```

Here, `get_timelock_amount_expensive_op` not only retrieves all fields
from storage, but puts in extra instructions construct the struct
in-memory.

`get_timelock_amount_cheap_op` on the other hand simply calls the
soroban host function `get_contract_data` and doesn't need extra work.

---------

Signed-off-by: salaheldinsoliman <salaheldin_sameh@aucegypt.edu>
salaheldinsoliman há 2 semanas atrás
pai
commit
9f0158515e

+ 9 - 9
.github/workflows/test.yml

@@ -50,7 +50,7 @@ jobs:
     - name: Install Rust
       uses: dtolnay/rust-toolchain@master
       with:
-        toolchain: 1.87.0
+        toolchain: 1.88.0
         components: |
           llvm-tools
           clippy
@@ -107,7 +107,7 @@ jobs:
       run: |
         sudo apt-get update
         sudo apt-get install -y gcc g++ make
-    - uses: dtolnay/rust-toolchain@1.87.0
+    - uses: dtolnay/rust-toolchain@1.88.0
     - name: Get LLVM
       run: curl -sSL --output llvm16.0-linux-arm64.tar.xz https://github.com/hyperledger-solang/solang-llvm/releases/download/llvm16-0/llvm16.0-linux-arm64.tar.xz
     - name: Extract LLVM
@@ -140,7 +140,7 @@ jobs:
     # Use C:\ as D:\ might run out of space
     - name: "Use C: for rust temporary files"
       run: echo "CARGO_TARGET_DIR=C:\target" | Out-File -FilePath $Env:GITHUB_ENV -Encoding utf8 -Append
-    - uses: dtolnay/rust-toolchain@1.87.0
+    - uses: dtolnay/rust-toolchain@1.88.0
       with:
         components: clippy
     # We run clippy on Linux in the lint job above, but this does not check #[cfg(windows)] items
@@ -166,7 +166,7 @@ jobs:
       uses: actions/checkout@v4
       with:
         submodules: recursive
-    - uses: dtolnay/rust-toolchain@1.87.0
+    - uses: dtolnay/rust-toolchain@1.88.0
     - name: Get LLVM
       run: curl -sSL --output llvm16.0-mac-arm.tar.xz https://github.com/hyperledger-solang/solang-llvm/releases/download/llvm16-0/llvm16.0-mac-arm.tar.xz
     - name: Extract LLVM
@@ -192,7 +192,7 @@ jobs:
       uses: actions/checkout@v4
       with:
         submodules: recursive
-    - uses: dtolnay/rust-toolchain@1.87.0
+    - uses: dtolnay/rust-toolchain@1.88.0
     - name: Get LLVM
       run: wget -q -O llvm16.0-mac-intel.tar.xz https://github.com/hyperledger-solang/solang-llvm/releases/download/llvm16-0/llvm16.0-mac-intel.tar.xz
     - name: Extract LLVM
@@ -254,7 +254,7 @@ jobs:
     - uses: actions/setup-node@v4
       with:
         node-version: '16'
-    - uses: dtolnay/rust-toolchain@1.87.0
+    - uses: dtolnay/rust-toolchain@1.88.0
     - name: Setup yarn
       run: npm install -g yarn
     - uses: actions/download-artifact@v4.1.8
@@ -309,7 +309,7 @@ jobs:
         with:
           node-version: '18'   # strongly recommended; 16 is EOL (works too, but less robust)
 
-      - uses: dtolnay/rust-toolchain@1.87.0
+      - uses: dtolnay/rust-toolchain@1.88.0
         with:
           target: wasm32-unknown-unknown
 
@@ -375,7 +375,7 @@ jobs:
     - uses: actions/setup-node@v4
       with:
         node-version: '16'
-    - uses: dtolnay/rust-toolchain@1.87.0
+    - uses: dtolnay/rust-toolchain@1.88.0
     - uses: actions/download-artifact@v4.1.8
       with:
         name: solang-linux-x86-64
@@ -552,7 +552,7 @@ jobs:
       - name: Install Rust
         uses: dtolnay/rust-toolchain@master
         with:
-          toolchain: 1.87.0
+          toolchain: 1.88.0
           components: llvm-tools
       - name: Install cargo-llvm-cov
         uses: taiki-e/install-action@cargo-llvm-cov

+ 1 - 1
Cargo.toml

@@ -8,7 +8,7 @@ license = "Apache-2.0"
 build = "build.rs"
 description = "Solang Solidity Compiler"
 keywords = [ "solidity", "compiler", "solana", "polkadot", "substrate" ]
-rust-version = "1.87.0"
+rust-version = "1.88.0"
 edition = "2021"
 exclude = [ "/.*", "/docs", "/solana-library", "/tests", "/integration", "/vscode", "/testdata" ]
 

+ 1 - 1
Dockerfile

@@ -4,7 +4,7 @@ COPY . src
 WORKDIR /src/stdlib/
 RUN make
 
-RUN rustup default 1.87.0
+RUN rustup default 1.88.0
 
 WORKDIR /src
 RUN cargo build --release

+ 2 - 1
solang-parser/src/lib.rs

@@ -25,7 +25,8 @@ mod tests;
     clippy::ptr_arg,
     clippy::just_underscores_and_digits,
     clippy::empty_line_after_outer_attr,
-    clippy::large_enum_variant
+    clippy::large_enum_variant,
+    clippy::uninlined_format_args
 )]
 mod solidity {
     include!(concat!(env!("OUT_DIR"), "/solidity.rs"));

+ 2 - 0
src/codegen/cfg.rs

@@ -2173,6 +2173,8 @@ impl Namespace {
     pub fn storage_type(&self) -> Type {
         if self.target == Target::Solana {
             Type::Uint(32)
+        } else if self.target == Target::Soroban {
+            Type::Uint(64)
         } else {
             Type::Uint(256)
         }

+ 7 - 3
src/codegen/dispatch/soroban.rs

@@ -92,7 +92,7 @@ pub fn function_dispatch(
             call_returns.push(new);
         }
 
-        let decoded = decode_args(&mut wrapper_cfg, &mut vartab);
+        let decoded = decode_args(&mut wrapper_cfg, &mut vartab, ns);
 
         // call storage initializer if needed
         if wrapper_cfg.name == "__constructor" {
@@ -145,7 +145,11 @@ pub fn function_dispatch(
     wrapper_cfgs
 }
 
-fn decode_args(wrapper_cfg: &mut ControlFlowGraph, vartab: &mut Vartable) -> Vec<Expression> {
+fn decode_args(
+    wrapper_cfg: &mut ControlFlowGraph,
+    vartab: &mut Vartable,
+    ns: &Namespace,
+) -> Vec<Expression> {
     let mut args = Vec::new();
 
     let params = wrapper_cfg.params.clone();
@@ -157,7 +161,7 @@ fn decode_args(wrapper_cfg: &mut ControlFlowGraph, vartab: &mut Vartable) -> Vec
             arg_no: i,
         };
 
-        let decoded = soroban_decode_arg(arg.clone(), wrapper_cfg, vartab);
+        let decoded = soroban_decode_arg(arg.clone(), wrapper_cfg, vartab, ns, None);
 
         args.push(decoded);
     }

+ 106 - 10
src/codegen/encoding/soroban_encoding.rs

@@ -6,7 +6,7 @@ use crate::codegen::encoding::create_encoder;
 use crate::codegen::vartable::Vartable;
 use crate::codegen::Expression;
 use crate::codegen::HostFunctions;
-use crate::sema::ast::{Namespace, RetrieveType, Type, Type::Uint};
+use crate::sema::ast::{Namespace, RetrieveType, StructType, Type, Type::Uint};
 use num_bigint::BigInt;
 use num_traits::Zero;
 use solang_parser::helpers::CodeLocation;
@@ -88,7 +88,7 @@ pub fn soroban_decode(
     _loc: &Loc,
     buffer: &Expression,
     _types: &[Type],
-    _ns: &Namespace,
+    ns: &Namespace,
     vartab: &mut Vartable,
     cfg: &mut ControlFlowGraph,
     _buffer_size_expr: Option<Expression>,
@@ -101,7 +101,7 @@ pub fn soroban_decode(
         expr: Box::new(buffer.clone()),
     };
 
-    let decoded_val = soroban_decode_arg(loaded_val, cfg, vartab);
+    let decoded_val = soroban_decode_arg(loaded_val, cfg, vartab, ns, None);
 
     returns.push(decoded_val);
 
@@ -112,11 +112,18 @@ pub fn soroban_decode_arg(
     arg: Expression,
     wrapper_cfg: &mut ControlFlowGraph,
     vartab: &mut Vartable,
+    ns: &Namespace,
+    decode_as: Option<Type>,
 ) -> Expression {
-    let ty = if let Type::Ref(inner_ty) = arg.ty() {
-        *inner_ty
-    } else {
-        arg.ty()
+    let ty = match decode_as {
+        Some(ty) => ty,
+        None => {
+            if let Type::Ref(inner_ty) = arg.ty() {
+                *inner_ty
+            } else {
+                arg.ty()
+            }
+        }
     };
 
     match ty {
@@ -189,6 +196,9 @@ pub fn soroban_decode_arg(
             }),
             signed: true,
         },
+        Type::Struct(StructType::UserDefined(n)) => {
+            decode_struct(arg, wrapper_cfg, vartab, n, ns, ty)
+        }
 
         _ => unimplemented!(),
     }
@@ -323,8 +333,6 @@ pub fn soroban_encode_arg(
                     _ => unreachable!(),
                 };
 
-                println!("encoded: {:?}, len: {:?}", encoded, len);
-
                 Instr::Call {
                     res: vec![obj],
                     return_tys: vec![Type::Uint(64)],
@@ -562,7 +570,16 @@ pub fn soroban_encode_arg(
                 expr: encoded,
             }
         }
-        _ => todo!("Type not yet supported"),
+        Type::Struct(StructType::UserDefined(n)) => {
+            let buf = encode_struct(item.clone(), cfg, vartab, ns, n);
+
+            Instr::Set {
+                loc: Loc::Codegen,
+                res: obj,
+                expr: buf,
+            }
+        }
+        _ => todo!("Type not yet supported in soroban encoder: {:?}", item.ty()),
     };
 
     cfg.add(vartab, ret);
@@ -919,3 +936,82 @@ fn extract_tag(arg: Expression) -> Expression {
         right: bit_mask.into(),
     }
 }
+
+/// encode a struct into a buffer where each field is 64 bits long soroban Val.
+fn encode_struct(
+    item: Expression,
+    cfg: &mut ControlFlowGraph,
+    vartab: &mut Vartable,
+    ns: &Namespace,
+    struct_no: usize,
+) -> Expression {
+    let fields = &ns.structs[struct_no].fields;
+    let mut fields_vars = Vec::new();
+
+    for (index, field) in fields.iter().enumerate() {
+        let field = Expression::StructMember {
+            loc: item.loc(),
+            ty: field.ty.clone(),
+            expr: Box::new(item.clone()),
+            member: index,
+        };
+
+        let actual_loaded_field = Expression::Load {
+            loc: Loc::Codegen,
+            ty: field.ty(),
+            expr: field.into(),
+        };
+
+        fields_vars.push(actual_loaded_field);
+    }
+
+    // now call soroban_encode for all fields
+    let ret = soroban_encode(&item.loc(), fields_vars, ns, vartab, cfg, false);
+
+    ret.0
+}
+
+/// Decode a struct from soroban encoding. Struct fields are laid out sequentially in a buffer, where each field is 64 bits long.
+fn decode_struct(
+    mut item: Expression,
+    cfg: &mut ControlFlowGraph,
+    vartab: &mut Vartable,
+    struct_no: usize,
+    ns: &Namespace,
+    struct_ty: Type,
+) -> Expression {
+    let tys = &ns.structs[struct_no]
+        .fields
+        .iter()
+        .map(|f| f.ty.clone())
+        .collect::<Vec<_>>();
+
+    let mut members = Vec::new();
+
+    for ty in tys {
+        let loaded_val = Expression::Load {
+            loc: Loc::Codegen,
+            ty: Uint(64),
+            expr: Box::new(item.clone()),
+        };
+
+        let decode_val = soroban_decode_arg(loaded_val, cfg, vartab, ns, Some(ty.clone()));
+
+        members.push(decode_val);
+
+        item = Expression::AdvancePointer {
+            pointer: Box::new(item),
+            bytes_offset: Box::new(Expression::NumberLiteral {
+                loc: Loc::Codegen,
+                ty: Uint(32),
+                value: BigInt::from(8),
+            }),
+        };
+    }
+
+    Expression::StructLiteral {
+        loc: Loc::Codegen,
+        ty: struct_ty,
+        values: members,
+    }
+}

+ 44 - 9
src/codegen/expression.rs

@@ -679,16 +679,51 @@ pub fn expression(
                         .sum()
                 };
 
-                Expression::Add {
-                    loc: *loc,
-                    ty: ns.storage_type(),
-                    overflowing: true,
-                    left: Box::new(expression(var, cfg, contract_no, func, ns, vartab, opt)),
-                    right: Box::new(Expression::NumberLiteral {
+                if ns.target == Target::Soroban {
+                    // In Soroban, storage struct members are accessed via a key whose representation is a Soroban Vec.
+                    // Therefore instead of adding the offset we insert it as a separate argument.
+
+                    let soroban_key = expression(var, cfg, contract_no, func, ns, vartab, opt);
+
+                    let offset = Expression::NumberLiteral {
                         loc: *loc,
-                        ty: ns.storage_type(),
+                        ty: Type::Uint(32),
                         value: offset,
-                    }),
+                    };
+
+                    let offset_encoded = soroban_encode_arg(offset, cfg, vartab, ns);
+
+                    let res = vartab.temp_name("vec_push_codegen", &Type::Uint(64));
+                    let var = Expression::Variable {
+                        loc: Loc::Codegen,
+                        ty: Type::Uint(64),
+                        var_no: res,
+                    };
+
+                    let enum_vec_put = Instr::Call {
+                        res: vec![res],
+                        return_tys: vec![Type::Uint(64)],
+                        call: InternalCallTy::HostFunction {
+                            name: HostFunctions::VecPushBack.name().to_string(),
+                        },
+                        args: vec![soroban_key, offset_encoded],
+                    };
+
+                    cfg.add(vartab, enum_vec_put);
+
+                    var
+                } else {
+                    Expression::Add {
+                        loc: *loc,
+                        ty: ns.storage_type(),
+                        overflowing: true,
+                        left: Box::new(expression(var, cfg, contract_no, func, ns, vartab, opt)),
+                        right: Box::new(Expression::NumberLiteral {
+                            loc: *loc,
+                            ty: ns.storage_type(),
+                            value: offset,
+                        }),
+                    }
                 }
             } else {
                 unreachable!();
@@ -4209,7 +4244,7 @@ pub fn load_storage(
     };
 
     if ns.target == Target::Soroban {
-        soroban_decode_arg(var, cfg, vartab)
+        soroban_decode_arg(var, cfg, vartab, ns, None)
     } else {
         var
     }

+ 6 - 0
src/codegen/mod.rs

@@ -123,6 +123,9 @@ pub enum HostFunctions {
     StringNewFromLinearMemory,
     StrKeyToAddr,
     GetCurrentContractAddress,
+    BytesNewFromLinearMemory,
+    BytesLen,
+    BytesCopyToLinearMemory,
 }
 
 impl HostFunctions {
@@ -155,6 +158,9 @@ impl HostFunctions {
             HostFunctions::StringNewFromLinearMemory => "b.i",
             HostFunctions::StrKeyToAddr => "a.1",
             HostFunctions::GetCurrentContractAddress => "x.7",
+            HostFunctions::BytesNewFromLinearMemory => "b.3",
+            HostFunctions::BytesLen => "b.8",
+            HostFunctions::BytesCopyToLinearMemory => "b.1",
         }
     }
 }

+ 0 - 1
src/emit/binary.rs

@@ -1073,7 +1073,6 @@ impl<'a> Binary<'a> {
             Some(s) => self.emit_global_string("const_string", s, true),
         };
 
-        println!("calling soroban alloc : {:?}", size);
         let allocator = if self.ns.target == Target::Soroban {
             self.builder.build_call(
                 self.module.get_function("soroban_alloc_init").unwrap(),

+ 7 - 2
src/emit/expression.rs

@@ -79,10 +79,16 @@ pub(super) fn expression<'a, T: TargetRuntime<'a> + ?Sized>(
         } => {
             let struct_ty = bin.llvm_type(ty);
 
+            let allocator = if bin.ns.target == Target::Soroban {
+                "soroban_malloc"
+            } else {
+                "__malloc"
+            };
+
             let s = bin
                 .builder
                 .build_call(
-                    bin.module.get_function("__malloc").unwrap(),
+                    bin.module.get_function(allocator).unwrap(),
                     &[struct_ty
                         .size_of()
                         .unwrap()
@@ -128,7 +134,6 @@ pub(super) fn expression<'a, T: TargetRuntime<'a> + ?Sized>(
             s.into()
         }
         Expression::BytesLiteral { value: bs, ty, .. } => {
-            println!("BytesLiteral: {:?} and ty {:?}", bs, ty);
             // If the type of a BytesLiteral is a String, embedd the bytes in the binary.
             if ty == &Type::String || ty == &Type::Address(true) {
                 let data = bin.emit_global_string("const_string", bs, true);

+ 1 - 1
src/emit/mod.rs

@@ -205,7 +205,7 @@ pub trait TargetRuntime<'a> {
     );
 
     /// Prints a string
-    fn print(&self, bin: &Binary, string: PointerValue, length: IntValue);
+    fn print<'b>(&self, bin: &Binary<'b>, string: PointerValue<'b>, length: IntValue<'b>);
 
     /// Return success without any result
     fn return_empty_abi(&self, bin: &Binary);

+ 1 - 1
src/emit/polkadot/target.rs

@@ -735,7 +735,7 @@ impl<'a> TargetRuntime<'a> for PolkadotTarget {
         bin.builder.build_unreachable().unwrap();
     }
 
-    fn print(&self, bin: &Binary, string_ptr: PointerValue, string_len: IntValue) {
+    fn print<'b>(&self, bin: &Binary<'b>, string_ptr: PointerValue<'b>, string_len: IntValue<'b>) {
         emit_context!(bin);
 
         call!("debug_message", &[string_ptr.into(), string_len.into()])

+ 1 - 1
src/emit/solana/target.rs

@@ -1235,7 +1235,7 @@ impl<'a> TargetRuntime<'a> for SolanaTarget {
             .unwrap();
     }
 
-    fn print(&self, bin: &Binary, string_ptr: PointerValue, string_len: IntValue) {
+    fn print<'b>(&self, bin: &Binary<'b>, string_ptr: PointerValue<'b>, string_len: IntValue<'b>) {
         let string_len64 = bin
             .builder
             .build_int_z_extend(string_len, bin.context.i64_type(), "")

+ 12 - 0
src/emit/soroban/mod.rs

@@ -109,6 +109,15 @@ impl HostFunctions {
                 .context
                 .i64_type()
                 .fn_type(&[ty.into(), ty.into()], false),
+            HostFunctions::BytesNewFromLinearMemory => bin
+                .context
+                .i64_type()
+                .fn_type(&[ty.into(), ty.into()], false),
+            HostFunctions::BytesLen => bin.context.i64_type().fn_type(&[ty.into()], false),
+            HostFunctions::BytesCopyToLinearMemory => bin
+                .context
+                .i64_type()
+                .fn_type(&[ty.into(), ty.into(), ty.into(), ty.into()], false),
         }
     }
 }
@@ -296,6 +305,7 @@ impl SorobanTarget {
                             ast::Type::Bytes(_) => ScSpecTypeDef::Bytes,
                             ast::Type::String => ScSpecTypeDef::String,
                             ast::Type::Void => ScSpecTypeDef::Void,
+                            ast::Type::Struct(_) => ScSpecTypeDef::Void, // TODO: Map struct types.
                             _ => panic!("unsupported return type {ty:?}"),
                         }
                     }) // TODO: Map type.
@@ -364,6 +374,8 @@ impl SorobanTarget {
             HostFunctions::StringNewFromLinearMemory,
             HostFunctions::StrKeyToAddr,
             HostFunctions::GetCurrentContractAddress,
+            HostFunctions::BytesNewFromLinearMemory,
+            HostFunctions::BytesCopyToLinearMemory,
         ];
 
         for func in &host_functions {

+ 236 - 12
src/emit/soroban/target.rs

@@ -56,6 +56,18 @@ impl<'a> TargetRuntime<'a> for SorobanTarget {
             *slot
         };
 
+        // If we are loading a struct, we need to load each field separately and put it in a buffer of this format: [ field1, field2, ... ] where each field is a Soroban tagged value of type i64
+        // We loop over each field, call GetContractData for each field and put it in the buffer
+        if let Type::Struct(ast::StructType::UserDefined(n)) = ty {
+            let field_count = &bin.ns.structs[*n].fields.len();
+
+            // call soroban_get_fields to get a buffer with all fields
+            let struct_buffer =
+                soroban_get_fields_to_val_buffer(bin, function, slot, *field_count, storage_type);
+
+            return struct_buffer.as_basic_value_enum();
+        }
+
         // === Call HasContractData ===
         let has_data_val = call!(
             HostFunctions::HasContractData.name(),
@@ -141,6 +153,25 @@ impl<'a> TargetRuntime<'a> for SorobanTarget {
             *slot
         };
 
+        // In case of struct, we receive a buffer in that format: [ field1, field2, ... ] where each field is a Soroban tagged value of type i64
+        // therefore, for each field, we need to extract it from the buffer and call PutContractData for each field separately
+        if let Type::Struct(ast::StructType::UserDefined(n)) = ty {
+            let field_count = &bin.ns.structs[*n].fields.len();
+
+            let data_ptr = bin.vector_bytes(dest);
+
+            // call soroban_put_fields for each field
+            soroban_put_fields_from_val_buffer(
+                bin,
+                function,
+                slot,
+                data_ptr,
+                *field_count,
+                storage_type,
+            );
+            return;
+        }
+
         let value = bin
             .builder
             .build_call(
@@ -256,22 +287,26 @@ impl<'a> TargetRuntime<'a> for SorobanTarget {
             .unwrap()
             .into_int_value();
 
-        let slot = if slot.is_const() {
-            slot.as_basic_value_enum()
-                .into_int_value()
-                .const_cast(bin.context.i64_type(), false)
-        } else {
-            slot
-        };
-
-        // push the slot to the vector
+        // push the slot to the vector as U32Val
+        let slot_encoded = encode_value(
+            if slot.get_type().get_bit_width() == 64 {
+                slot
+            } else {
+                bin.builder
+                    .build_int_z_extend(slot, bin.context.i64_type(), "slot64")
+                    .unwrap()
+            },
+            32,
+            4,
+            bin,
+        );
         let res = bin
             .builder
             .build_call(
                 bin.module
                     .get_function(HostFunctions::VecPushBack.name())
                     .unwrap(),
-                &[vec_new.as_basic_value_enum().into(), slot.into()],
+                &[vec_new.as_basic_value_enum().into(), slot_encoded.into()],
                 "push",
             )
             .unwrap()
@@ -344,7 +379,7 @@ impl<'a> TargetRuntime<'a> for SorobanTarget {
 
     /// Prints a string
     /// TODO: Implement this function, with a call to the `log` function in the Soroban runtime.
-    fn print(&self, bin: &Binary, string: PointerValue, length: IntValue) {
+    fn print<'b>(&self, bin: &Binary<'b>, string: PointerValue<'b>, length: IntValue<'b>) {
         let msg_pos = bin
             .builder
             .build_ptr_to_int(string, bin.context.i64_type(), "msg_pos")
@@ -755,7 +790,7 @@ fn encode_value<'a>(
     mut value: IntValue<'a>,
     shift: u64,
     add: u64,
-    bin: &'a Binary,
+    bin: &Binary<'a>,
 ) -> IntValue<'a> {
     match value.get_type().get_bit_width() {
         32 =>
@@ -831,3 +866,192 @@ pub fn type_to_tagged_zero_val<'ctx>(bin: &Binary<'ctx>, ty: &Type) -> IntValue<
     let tag_val: u64 = tag;
     i64_type.const_int(tag_val, false)
 }
+
+/// Given a linear-memory buffer of consecutive Soroban Val-encoded i64 fields
+/// [field0, field1, ...], push a field index onto `base_key_vec` and call
+/// PutContractData for each field separately.
+///
+/// - `base_key_vec`: i64 Val for a Soroban Vector key (e.g., [slot, mapping_index])
+/// - `buffer_ptr`: pointer to the first byte of the buffer (i8*)
+/// - `field_count`: number of 64-bit Val entries in the buffer
+/// - `storage_type`: Soroban storage type tag (Temporary/Persistent/Instance)
+pub fn soroban_put_fields_from_val_buffer<'a>(
+    bin: &Binary<'a>,
+    _function: FunctionValue<'a>,
+    base_key_vec: IntValue<'a>,
+    buffer_ptr: PointerValue<'a>,
+    field_count: usize,
+    storage_type: u64,
+) {
+    emit_context!(bin);
+
+    let i64_t = bin.context.i64_type();
+
+    for i in 0..field_count {
+        // Compute pointer to field i: buffer_ptr + i * 8
+        let byte_offset = i64_t.const_int(i as u64, false);
+        let field_byte_ptr = unsafe {
+            bin.builder
+                .build_gep(i64_t, buffer_ptr, &[byte_offset], "field_byte_ptr")
+                .unwrap()
+        };
+
+        // Cast to i64* and load the Val-encoded i64
+        let field_val_i64 = bin
+            .builder
+            .build_load(i64_t, field_byte_ptr, "field_val_i64")
+            .unwrap()
+            .into_int_value();
+
+        // Extend key with field index: push U32Val(i)
+        let idx_u32 = bin.context.i32_type().const_int(i as u64, false);
+        let idx_val = encode_value(idx_u32, 32, 4, bin);
+        let field_key_vec = bin
+            .builder
+            .build_call(
+                bin.module
+                    .get_function(HostFunctions::VecPushBack.name())
+                    .unwrap(),
+                &[base_key_vec.into(), idx_val.into()],
+                "key_push_field",
+            )
+            .unwrap()
+            .try_as_basic_value()
+            .left()
+            .unwrap()
+            .into_int_value();
+
+        // Store this field value under the extended key
+        // storage_type is a plain u64 here
+        let storage_ty_val = bin.context.i64_type().const_int(storage_type, false);
+        let _ = bin
+            .builder
+            .build_call(
+                bin.module
+                    .get_function(HostFunctions::PutContractData.name())
+                    .unwrap(),
+                &[
+                    field_key_vec.into(),
+                    field_val_i64.into(),
+                    storage_ty_val.into(),
+                ],
+                "put_field_from_buffer",
+            )
+            .unwrap();
+    }
+}
+
+/// Fetch each field value from storage using `base_key_vec` extended with the field index,
+/// and write them into a freshly allocated linear buffer as consecutive i64 Soroban Vals.
+/// Returns a pointer to `struct.vector` whose payload size is `field_count * 8` bytes.
+pub fn soroban_get_fields_to_val_buffer<'a>(
+    bin: &Binary<'a>,
+    function: FunctionValue<'a>,
+    base_key_vec: IntValue<'a>,
+    field_count: usize,
+    storage_type: u64,
+) -> PointerValue<'a> {
+    emit_context!(bin);
+
+    // Allocate zero-initialized buffer
+    let size_bytes = bin
+        .context
+        .i32_type()
+        .const_int((field_count as u64) * 8, false);
+
+    let vec_ptr = bin
+        .builder
+        .build_call(
+            bin.module.get_function("soroban_malloc").unwrap(),
+            &[size_bytes.into()],
+            "soroban_malloc",
+        )
+        .unwrap()
+        .try_as_basic_value()
+        .left()
+        .unwrap()
+        .into_pointer_value();
+
+    let storage_ty_i64 = bin.context.i64_type().const_int(storage_type, false);
+
+    for i in 0..field_count {
+        // key = base_key_vec ++ U32Val(i)
+        let idx_u32 = bin.context.i32_type().const_int(i as u64, false);
+        let idx_val = encode_value(idx_u32, 32, 4, bin);
+        let field_key_vec = bin
+            .builder
+            .build_call(
+                bin.module
+                    .get_function(HostFunctions::VecPushBack.name())
+                    .unwrap(),
+                &[base_key_vec.into(), idx_val.into()],
+                "key_push_field",
+            )
+            .unwrap()
+            .try_as_basic_value()
+            .left()
+            .unwrap()
+            .into_int_value();
+
+        // has = HasContractData(key, storage_type)
+        let has_val = bin
+            .builder
+            .build_call(
+                bin.module
+                    .get_function(HostFunctions::HasContractData.name())
+                    .unwrap(),
+                &[field_key_vec.into(), storage_ty_i64.into()],
+                "has_field",
+            )
+            .unwrap()
+            .try_as_basic_value()
+            .left()
+            .unwrap()
+            .into_int_value();
+        let cond = is_val_true(bin, has_val);
+
+        // Blocks
+        let then_bb = bin.context.append_basic_block(function, "load_field");
+        let else_bb = bin.context.append_basic_block(function, "skip_field");
+        let cont_bb = bin.context.append_basic_block(function, "cont_field");
+
+        bin.builder
+            .build_conditional_branch(cond, then_bb, else_bb)
+            .unwrap();
+
+        // THEN: fetch and store val into buffer[i]
+        bin.builder.position_at_end(then_bb);
+        let val_i64 = bin
+            .builder
+            .build_call(
+                bin.module
+                    .get_function(HostFunctions::GetContractData.name())
+                    .unwrap(),
+                &[field_key_vec.into(), storage_ty_i64.into()],
+                "get_field",
+            )
+            .unwrap()
+            .try_as_basic_value()
+            .left()
+            .unwrap()
+            .into_int_value();
+        let idx64 = bin.context.i64_type().const_int((i) as u64, false);
+        let elem_ptr = unsafe {
+            bin.builder
+                .build_gep(bin.context.i64_type(), vec_ptr, &[idx64], "elem_ptr")
+                .unwrap()
+        };
+        bin.builder.build_store(elem_ptr, val_i64).unwrap();
+        bin.builder.build_unconditional_branch(cont_bb).unwrap();
+
+        // ELSE: leave zero (already zero-initialized)
+        bin.builder.position_at_end(else_bb);
+        bin.builder.build_unconditional_branch(cont_bb).unwrap();
+
+        // CONT
+        bin.builder.position_at_end(cont_bb);
+    }
+
+    //bin.vector_bytes(  vec_ptr.as_basic_value_enum())
+    vec_ptr
+}

+ 2 - 4
src/sema/ast.rs

@@ -151,8 +151,7 @@ impl Type {
                 let rounded_width = Self::get_soroban_int_width(*width);
                 if rounded_width != *width {
                     let message = format!(
-                        "int{} is not supported by the Soroban runtime and will be rounded up to int{}",
-                        width, rounded_width
+                        "int{width} is not supported by the Soroban runtime and will be rounded up to int{rounded_width}"
                     );
                     if ns.strict_soroban_types {
                         ns.diagnostics.push(Diagnostic::error(loc, message));
@@ -168,8 +167,7 @@ impl Type {
                 let rounded_width = Self::get_soroban_int_width(*width);
                 if rounded_width != *width {
                     let message = format!(
-                        "uint{} is not supported by the Soroban runtime and will be rounded up to uint{}",
-                        width, rounded_width
+                        "uint{width} is not supported by the Soroban runtime and will be rounded up to uint{rounded_width}"
                     );
                     if ns.strict_soroban_types {
                         ns.diagnostics.push(Diagnostic::error(loc, message));

+ 5 - 3
stdlib/soroban.c

@@ -117,11 +117,13 @@ static void *alloc_impl(uint32_t bytes, uint32_t align)
 }
 
 // -------------------- exported API --------------------
+// Forward declare so soroban_alloc can delegate to it
+struct vector *soroban_alloc_init(uint32_t members, const void *init_ptr);
 
-__attribute__((export_name("soroban_alloc"))) void *soroban_alloc(uint32_t size)
+__attribute__((export_name("soroban_alloc"))) struct vector *soroban_alloc(uint32_t members)
 {
-    // default alignment 8
-    return alloc_impl(size, 8);
+    // Delegate to soroban_alloc_init with empty initializer
+    return soroban_alloc_init(members, (const void *)0);
 }
 
 __attribute__((export_name("soroban_alloc_init"))) struct vector *soroban_alloc_init(uint32_t members,

+ 1 - 1
tests/soroban_testcases/alloc.rs

@@ -58,7 +58,7 @@ fn arrays_basic_ops_test() {
     // push_pop(): [5,10] -> pop -> [5]; len(=1) + mylist[0](=5) = 6
     let expected: Val = 6_u64.into_val(&runtime.env);
     let res = runtime.invoke_contract(addr, "push_pop", vec![]);
-    println!("Result of push_pop: {:?}", res);
+    println!("Result of push_pop: {res:?}");
     assert!(expected.shallow_eq(&res));
 
     // loop(): 5 + 10 + 15 = 30

+ 1 - 0
tests/soroban_testcases/mod.rs

@@ -9,5 +9,6 @@ mod mappings;
 mod math;
 mod print;
 mod storage;
+mod structs;
 mod token;
 mod ttl;

+ 234 - 0
tests/soroban_testcases/structs.rs

@@ -0,0 +1,234 @@
+// SPDX-License-Identifier: Apache-2.0
+
+use crate::build_solidity;
+use indexmap::Equivalent;
+use soroban_sdk::{testutils::Address as _, Address, FromVal, IntoVal, Val};
+
+#[test]
+fn get_fields_via_dot() {
+    let runtime = build_solidity(
+        r#"
+        contract locker {
+            struct Lock {
+                uint64 release_time;
+                address beneficiary;
+                uint64 amount;
+            }
+
+            mapping(address => Lock) locks;
+
+            function create_lock(
+                uint64 release_time,
+                address beneficiary,
+                uint64 amount
+            ) public returns (uint64) {
+                Lock memory l = Lock({
+                    release_time: release_time,
+                    beneficiary: beneficiary,
+                    amount: amount
+                });
+
+                locks[beneficiary] = l;
+                return l.amount;
+            }
+
+            function get_lock_amount(address beneficiary) public view returns (uint64) {
+                return locks[beneficiary].amount;
+            }
+
+            function get_lock_release(address beneficiary) public view returns (uint64) {
+                return locks[beneficiary].release_time;
+            }
+
+            function get_lock_beneficiary(address key) public view returns (address) {
+                return locks[key].beneficiary;
+            }
+
+            // Extended functionality: increase amount in-place and return new total
+            function increase_lock_amount(address beneficiary, uint64 delta) public returns (uint64) {
+                locks[beneficiary].amount += delta;
+                return locks[beneficiary].amount;
+            }
+
+            // Extended functionality: move a lock to a different beneficiary
+            function move_lock(address from, address to) public {
+                Lock memory l = locks[from];
+                require(l.amount != 0, "no lock");
+                l.beneficiary = to;
+                locks[to] = l;
+                // emulate delete by zeroing fields
+                locks[from].amount = 0;
+                locks[from].release_time = 0;
+            }
+
+            // Extended functionality: clear lock for a beneficiary
+            function clear_lock(address beneficiary) public {
+                // emulate delete by zeroing fields
+                locks[beneficiary].amount = 0;
+                locks[beneficiary].release_time = 0;
+            }
+        }
+        "#,
+        |_| {},
+    );
+
+    let addr = runtime.contracts.last().unwrap();
+
+    let user1 = Address::generate(&runtime.env);
+    let user2 = Address::generate(&runtime.env);
+
+    let release_time: Val = 1_000_u64.into_val(&runtime.env);
+    let amount: Val = 500_u64.into_val(&runtime.env);
+
+    // Create a new lock for user1
+    let create_args = vec![release_time, user1.clone().into_val(&runtime.env), amount];
+    let res = runtime.invoke_contract(addr, "create_lock", create_args);
+    assert!(amount.shallow_eq(&res));
+
+    // Verify getters
+    let get_amt_args = vec![user1.clone().into_val(&runtime.env)];
+    let get_rel_args = vec![user1.clone().into_val(&runtime.env)];
+    let get_ben_args = vec![user1.clone().into_val(&runtime.env)];
+    let got_amount = runtime.invoke_contract(addr, "get_lock_amount", get_amt_args);
+    let got_release = runtime.invoke_contract(addr, "get_lock_release", get_rel_args);
+    let got_beneficiary = runtime.invoke_contract(addr, "get_lock_beneficiary", get_ben_args);
+    assert!(amount.shallow_eq(&got_amount));
+    assert!(release_time.shallow_eq(&got_release));
+    let addr_val = Address::from_val(&runtime.env, &got_beneficiary);
+    assert!(addr_val.equivalent(&user1));
+
+    // Increase amount and verify new total
+    let delta: Val = 250_u64.into_val(&runtime.env);
+    let inc_args = vec![user1.clone().into_val(&runtime.env), delta];
+    let new_total = runtime.invoke_contract(addr, "increase_lock_amount", inc_args);
+    let expected_total: Val = 750_u64.into_val(&runtime.env);
+    assert!(expected_total.shallow_eq(&new_total));
+
+    // Move lock from user1 to user2
+    let move_args = vec![
+        user1.clone().into_val(&runtime.env),
+        user2.clone().into_val(&runtime.env),
+    ];
+    let _ = runtime.invoke_contract(addr, "move_lock", move_args);
+
+    // After moving, user1 should have no lock (amount == 0)
+    let zero: Val = 0_u64.into_val(&runtime.env);
+    let amt_user1 = runtime.invoke_contract(
+        addr,
+        "get_lock_amount",
+        vec![user1.clone().into_val(&runtime.env)],
+    );
+    assert!(zero.shallow_eq(&amt_user1));
+
+    // And user2 should now hold the moved lock with the updated total amount
+    let amt_user2 = runtime.invoke_contract(
+        addr,
+        "get_lock_amount",
+        vec![user2.clone().into_val(&runtime.env)],
+    );
+    assert!(expected_total.shallow_eq(&amt_user2));
+
+    // Beneficiary for user2's lock should be user2
+    let ben_user2 = runtime.invoke_contract(
+        addr,
+        "get_lock_beneficiary",
+        vec![user2.clone().into_val(&runtime.env)],
+    );
+    let ben2 = Address::from_val(&runtime.env, &ben_user2);
+    assert!(ben2.equivalent(&user2));
+
+    // Clear user2's lock and verify
+    let _ = runtime.invoke_contract(
+        addr,
+        "clear_lock",
+        vec![user2.clone().into_val(&runtime.env)],
+    );
+    let amt_user2_after_clear =
+        runtime.invoke_contract(addr, "get_lock_amount", vec![user2.into_val(&runtime.env)]);
+    assert!(zero.shallow_eq(&amt_user2_after_clear));
+}
+
+// Removed: keep only two tests as requested
+
+#[test]
+fn get_whole_struct() {
+    let runtime = build_solidity(
+        r#"
+        contract locker {
+            struct Lock {
+                uint64 release_time;
+                address beneficiary;
+                uint64 amount;
+            }
+
+            mapping(address => Lock) locks;
+
+            function create_lock(
+                uint64 release_time,
+                address beneficiary,
+                uint64 amount
+            ) public returns (uint64) {
+                Lock memory l = Lock({
+                    release_time: release_time,
+                    beneficiary: beneficiary,
+                    amount: amount
+                });
+
+                locks[beneficiary] = l;
+                return l.amount;
+            }
+
+            function get_lock_amount(address beneficiary) public view returns (uint64) {
+                return locks[beneficiary].amount;
+            }
+
+            function get_lock_release(address beneficiary) public view returns (uint64) {
+                return locks[beneficiary].release_time;
+            }
+
+            function get_lock_beneficiary(address key) public view returns (address) {
+                return locks[key].beneficiary;
+            }
+        }
+        "#,
+        |_| {},
+    );
+
+    let addr = runtime.contracts.last().unwrap();
+
+    let user = Address::generate(&runtime.env);
+    let release_time: Val = 42_u64.into_val(&runtime.env);
+    let amount: Val = 7_u64.into_val(&runtime.env);
+
+    // Create lock
+    let _ = runtime.invoke_contract(
+        addr,
+        "create_lock",
+        vec![release_time, user.clone().into_val(&runtime.env), amount],
+    );
+
+    // Retrieve each field via accessors (no multiple returns)
+    let rt_val = runtime.invoke_contract(
+        addr,
+        "get_lock_release",
+        vec![user.clone().into_val(&runtime.env)],
+    );
+    let ben_val = runtime.invoke_contract(
+        addr,
+        "get_lock_beneficiary",
+        vec![user.clone().into_val(&runtime.env)],
+    );
+    let amt_val = runtime.invoke_contract(
+        addr,
+        "get_lock_amount",
+        vec![user.clone().into_val(&runtime.env)],
+    );
+
+    let rt: u64 = FromVal::from_val(&runtime.env, &rt_val);
+    let ben = Address::from_val(&runtime.env, &ben_val);
+    let amt: u64 = FromVal::from_val(&runtime.env, &amt_val);
+
+    assert_eq!(rt, 42);
+    assert!(ben.equivalent(&user));
+    assert_eq!(amt, 7);
+}