Browse Source

Soroban: support dynamic memory arrays (#1838)

This PR adds support for dynamic memory arrays is Soroban's Solidity.
Namely, this example:
http://github.com/stellar/soroban-examples/blob/main/alloc/src/lib.rs

Under the hood, the example above uses a custom no-free bump allocator,
written in Rust and linked with Soroban contracts:
https://github.com/stellar/rs-soroban-sdk/blob/main/soroban-sdk/src/alloc.rs#L5

We followed the same approach, but wrote it in C instead. The reason for
opting for C instead of Rust is that it fits well with Solang's
compiling and linking pipeline.

---------

Signed-off-by: salaheldinsoliman <salaheldin_sameh@aucegypt.edu>
salaheldinsoliman 1 month ago
parent
commit
fcc1e4b313

+ 4 - 2
src/codegen/encoding/soroban_encoding.rs

@@ -31,7 +31,7 @@ pub fn soroban_encode(
 
     let size_expr = Expression::NumberLiteral {
         loc: *loc,
-        ty: Uint(64),
+        ty: Uint(32),
         value: size.into(),
     };
     let encoded_bytes = vartab.temp_name("abi_encoded", &Type::Bytes(size as u8));
@@ -40,7 +40,7 @@ pub fn soroban_encode(
         loc: *loc,
         ty: Type::Bytes(size as u8),
         size: size_expr.clone().into(),
-        initializer: Some(vec![]),
+        initializer: None,
     };
 
     cfg.add(
@@ -323,6 +323,8 @@ pub fn soroban_encode_arg(
                     _ => unreachable!(),
                 };
 
+                println!("encoded: {:?}, len: {:?}", encoded, len);
+
                 Instr::Call {
                     res: vec![obj],
                     return_tys: vec![Type::Uint(64)],

+ 39 - 75
src/emit/binary.rs

@@ -292,6 +292,27 @@ impl<'a> Binary<'a> {
         export_list.push("__lshrti3");
         export_list.push("__ashrti3");
 
+        if self.ns.target == crate::Target::Soroban {
+            let mut f = self.module.get_first_function();
+            while let Some(func) = f {
+                let name = func.get_name().to_str().unwrap();
+                // leave LLVM intrinsics alone
+                if name.starts_with("llvm.") {
+                    f = func.get_next_function();
+                    continue;
+                }
+
+                if export_list.contains(&name) {
+                    func.set_linkage(inkwell::module::Linkage::External);
+                } else {
+                    func.set_linkage(inkwell::module::Linkage::Internal);
+                }
+
+                f = func.get_next_function();
+            }
+            return;
+        }
+
         while let Some(f) = func {
             let name = f.get_name().to_str().unwrap();
 
@@ -1036,71 +1057,7 @@ impl<'a> Binary<'a> {
         size: IntValue<'a>,
         elem_size: IntValue<'a>,
         init: Option<&Vec<u8>>,
-        ty: &Type,
     ) -> BasicValueEnum<'a> {
-        if self.ns.target == Target::Soroban {
-            if matches!(ty, Type::Bytes(_)) {
-                let n = if let Type::Bytes(n) = ty {
-                    n
-                } else {
-                    unreachable!()
-                };
-
-                let data = self
-                    .builder
-                    .build_alloca(self.context.i64_type().array_type((*n / 8) as u32), "data")
-                    .unwrap();
-
-                let ty = self.context.struct_type(
-                    &[data.get_type().into(), self.context.i64_type().into()],
-                    false,
-                );
-
-                // Start with an undefined struct value
-                let mut struct_value = ty.get_undef();
-
-                // Insert `data` into the first field of the struct
-                struct_value = self
-                    .builder
-                    .build_insert_value(struct_value, data, 0, "insert_data")
-                    .unwrap()
-                    .into_struct_value();
-
-                // Insert `size` into the second field of the struct
-                struct_value = self
-                    .builder
-                    .build_insert_value(struct_value, size, 1, "insert_size")
-                    .unwrap()
-                    .into_struct_value();
-
-                // Return the constructed struct value
-                return struct_value.into();
-            } else if matches!(ty, Type::String) {
-                let default = " ".as_bytes().to_vec();
-                let bs = init.unwrap_or(&default);
-
-                let data = self.emit_global_string("const_string", bs, true);
-
-                // A constant string, or array, is represented by a struct with two fields: a pointer to the data, and its length.
-                let ty = self.context.struct_type(
-                    &[
-                        self.context.ptr_type(AddressSpace::default()).into(),
-                        self.context.i64_type().into(),
-                    ],
-                    false,
-                );
-
-                return ty
-                    .const_named_struct(&[
-                        data.into(),
-                        self.context
-                            .i64_type()
-                            .const_int(bs.len() as u64, false)
-                            .into(),
-                    ])
-                    .as_basic_value_enum();
-            }
-        }
         if let Some(init) = init {
             if init.is_empty() {
                 return self
@@ -1116,16 +1073,22 @@ impl<'a> Binary<'a> {
             Some(s) => self.emit_global_string("const_string", s, true),
         };
 
-        self.builder
-            .build_call(
+        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(),
+                &[size.into(), init.into()],
+                "soroban_alloc",
+            )
+        } else {
+            self.builder.build_call(
                 self.module.get_function("vector_new").unwrap(),
                 &[size.into(), elem_size.into(), init.into()],
-                "",
+                "vector_new",
             )
-            .unwrap()
-            .try_as_basic_value()
-            .left()
-            .unwrap()
+        };
+
+        allocator.unwrap().try_as_basic_value().left().unwrap()
     }
 
     /// Number of element in a vector
@@ -1134,11 +1097,11 @@ impl<'a> Binary<'a> {
             // slice
             let slice = vector.into_struct_value();
 
-            let len_type = if self.ns.target == Target::Soroban {
+            /*let len_type = if self.ns.target == Target::Soroban {
                 self.context.i64_type()
             } else {
                 self.context.i32_type()
-            };
+            };*/
 
             self.builder
                 .build_int_truncate(
@@ -1146,7 +1109,7 @@ impl<'a> Binary<'a> {
                         .build_extract_value(slice, 1, "slice_len")
                         .unwrap()
                         .into_int_value(),
-                    len_type,
+                    self.context.i32_type(),
                     "len",
                 )
                 .unwrap()
@@ -1376,11 +1339,12 @@ static BPF_IR: [&[u8]; 6] = [
     include_bytes!("../../target/bpf/heap.bc"),
 ];
 
-static WASM_IR: [&[u8]; 4] = [
+static WASM_IR: [&[u8]; 5] = [
     include_bytes!("../../target/wasm/stdlib.bc"),
     include_bytes!("../../target/wasm/heap.bc"),
     include_bytes!("../../target/wasm/bigint.bc"),
     include_bytes!("../../target/wasm/format.bc"),
+    include_bytes!("../../target/wasm/soroban.bc"),
 ];
 
 static RIPEMD160_IR: &[u8] = include_bytes!("../../target/wasm/ripemd160.bc");

+ 2 - 1
src/emit/expression.rs

@@ -128,6 +128,7 @@ 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);
@@ -1580,7 +1581,7 @@ pub(super) fn expression<'a, T: TargetRuntime<'a> + ?Sized>(
                     .unwrap()
                     .const_cast(bin.context.i32_type(), false);
 
-                bin.vector_new(size, elem_size, initializer.as_ref(), ty)
+                bin.vector_new(size, elem_size, initializer.as_ref())
             }
         }
         Expression::Builtin {

+ 12 - 2
src/emit/instructions.rs

@@ -211,11 +211,16 @@ pub(super) fn process_instruction<'a, T: TargetRuntime<'a> + ?Sized>(
             let size = bin.builder.build_int_mul(elem_size, new_len, "").unwrap();
             let size = bin.builder.build_int_add(size, vec_size, "").unwrap();
 
+            let allocator = if bin.ns.target == Target::Soroban {
+                "soroban_realloc"
+            } else {
+                "__realloc"
+            };
             // Reallocate and reassign the array pointer
             let new = bin
                 .builder
                 .build_call(
-                    bin.module.get_function("__realloc").unwrap(),
+                    bin.module.get_function(allocator).unwrap(),
                     &[arr.into(), size.into()],
                     "",
                 )
@@ -382,11 +387,16 @@ pub(super) fn process_instruction<'a, T: TargetRuntime<'a> + ?Sized>(
                 w.vars.get_mut(res).unwrap().value = ret_val;
             }
 
+            let allocator = if bin.ns.target == Target::Soroban {
+                "soroban_realloc"
+            } else {
+                "__realloc"
+            };
             // Reallocate and reassign the array pointer
             let new = bin
                 .builder
                 .build_call(
-                    bin.module.get_function("__realloc").unwrap(),
+                    bin.module.get_function(allocator).unwrap(),
                     &[a.into(), size.into()],
                     "",
                 )

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

@@ -749,9 +749,7 @@ impl<'a> TargetRuntime<'a> for SolanaTarget {
                         .unwrap()
                         .into_int_value();
 
-                    dest = bin
-                        .vector_new(length, elem_size, None, elem_ty)
-                        .into_pointer_value();
+                    dest = bin.vector_new(length, elem_size, None).into_pointer_value();
                 };
 
                 let elem_size = elem_ty.solana_storage_size(bin.ns).to_u64().unwrap();

+ 42 - 28
src/emit/soroban/target.rs

@@ -345,33 +345,28 @@ 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) {
-        if string.is_const() && length.is_const() {
-            let msg_pos = bin
-                .builder
-                .build_ptr_to_int(string, bin.context.i64_type(), "msg_pos")
-                .unwrap();
-            let length = length.const_cast(bin.context.i64_type(), false);
+        let msg_pos = bin
+            .builder
+            .build_ptr_to_int(string, bin.context.i64_type(), "msg_pos")
+            .unwrap();
 
-            let msg_pos_encoded = encode_value(msg_pos, 32, 4, bin);
-            let length_encoded = encode_value(length, 32, 4, bin);
+        let msg_pos_encoded = encode_value(msg_pos, 32, 4, bin);
+        let length_encoded = encode_value(length, 32, 4, bin);
 
-            bin.builder
-                .build_call(
-                    bin.module
-                        .get_function(HostFunctions::LogFromLinearMemory.name())
-                        .unwrap(),
-                    &[
-                        msg_pos_encoded.into(),
-                        length_encoded.into(),
-                        msg_pos_encoded.into(),
-                        encode_value(bin.context.i64_type().const_zero(), 32, 4, bin).into(),
-                    ],
-                    "log",
-                )
-                .unwrap();
-        } else {
-            todo!("Dynamic String printing is not yet supported")
-        }
+        bin.builder
+            .build_call(
+                bin.module
+                    .get_function(HostFunctions::LogFromLinearMemory.name())
+                    .unwrap(),
+                &[
+                    msg_pos_encoded.into(),
+                    length_encoded.into(),
+                    msg_pos_encoded.into(),
+                    encode_value(bin.context.i64_type().const_zero(), 32, 4, bin).into(),
+                ],
+                "log",
+            )
+            .unwrap();
     }
 
     /// Return success without any result
@@ -452,7 +447,7 @@ impl<'a> TargetRuntime<'a> for SorobanTarget {
             .builder
             .build_int_unsigned_div(
                 payload_len,
-                bin.context.i64_type().const_int(8, false),
+                payload_len.get_type().const_int(8, false),
                 "args_len",
             )
             .unwrap();
@@ -461,7 +456,7 @@ impl<'a> TargetRuntime<'a> for SorobanTarget {
             .builder
             .build_int_sub(
                 args_len,
-                bin.context.i64_type().const_int(1, false),
+                args_len.get_type().const_int(1, false),
                 "args_len",
             )
             .unwrap();
@@ -756,7 +751,25 @@ fn storage_type_to_int(storage_type: &Option<StorageType>) -> u64 {
     }
 }
 
-fn encode_value<'a>(value: IntValue<'a>, shift: u64, add: u64, bin: &'a Binary) -> IntValue<'a> {
+fn encode_value<'a>(
+    mut value: IntValue<'a>,
+    shift: u64,
+    add: u64,
+    bin: &'a Binary,
+) -> IntValue<'a> {
+    match value.get_type().get_bit_width() {
+        32 =>
+        // extend to 64 bits
+        {
+            value = bin
+                .builder
+                .build_int_z_extend(value, bin.context.i64_type(), "temp")
+                .unwrap();
+        }
+        64 => (),
+        _ => unreachable!(),
+    }
+
     let shifted = bin
         .builder
         .build_left_shift(
@@ -765,6 +778,7 @@ fn encode_value<'a>(value: IntValue<'a>, shift: u64, add: u64, bin: &'a Binary)
             "temp",
         )
         .unwrap();
+
     bin.builder
         .build_int_add(
             shifted,

+ 1 - 6
src/emit/strings.rs

@@ -95,12 +95,7 @@ pub(super) fn format_string<'a, T: TargetRuntime<'a> + ?Sized>(
 
     // allocate the string and
     let vector = bin
-        .vector_new(
-            length,
-            bin.context.i32_type().const_int(1, false),
-            None,
-            &Type::String,
-        )
+        .vector_new(length, bin.context.i32_type().const_int(1, false), None)
         .into_pointer_value();
 
     let output_start = bin.vector_bytes(vector.into());

+ 11 - 5
src/linker/soroban_wasm.rs

@@ -24,13 +24,13 @@ pub fn link(input: &[u8], name: &str) -> Vec<u8> {
         .write_all(input)
         .expect("failed to write object file to temp file");
 
+    // Assemble wasm-ld command line
     let mut command_line = vec![
         CString::new("--no-entry").unwrap(),
         CString::new("--allow-undefined").unwrap(),
         CString::new("--gc-sections").unwrap(),
         CString::new("--global-base=0").unwrap(),
         CString::new("--initial-memory=1048576").unwrap(), // 1 MiB initial memory
-        CString::new("--max-memory=1048576").unwrap(),
     ];
     command_line.push(CString::new("--export-dynamic").unwrap());
 
@@ -94,11 +94,17 @@ fn generate_import_section(section: SectionLimited<Import>, module: &mut Module)
             }),
             _ => panic!("unexpected WASM import section {import:?}"),
         };
-        let module_name = import.name.split('.').next().unwrap();
-        // parse the import name to all string after the the first dot
-        let import_name = import.name.split('.').nth(1).unwrap();
-        imports.import(module_name, import_name, import_type);
+
+        // Handle both "module.func" and plain "name" (with module in import.module)
+        if let Some((mod_name, func_name)) = import.name.split_once('.') {
+            // Soroban-native shape: modulename.funcname
+            imports.import(mod_name, func_name, import_type);
+        } else {
+            //imports.import(import.module, &import.name, import_type);
+            unreachable!()
+        }
     }
+
     module.section(&imports);
 }
 

+ 1 - 1
stdlib/Makefile

@@ -9,7 +9,7 @@ CFLAGS=$(TARGET_FLAGS) -emit-llvm -O3 -ffreestanding -fno-builtin -Wall -Wno-unu
 	$(CC) -c $(CFLAGS) $< -o $@
 
 SOLANA=$(addprefix ../target/bpf/,solana.bc bigint.bc format.bc stdlib.bc ripemd160.bc heap.bc)
-WASM=$(addprefix ../target/wasm/,ripemd160.bc stdlib.bc bigint.bc format.bc heap.bc)
+WASM=$(addprefix ../target/wasm/,ripemd160.bc stdlib.bc bigint.bc format.bc heap.bc soroban.bc)
 
 all: $(SOLANA) $(WASM)
 

+ 222 - 0
stdlib/soroban.c

@@ -0,0 +1,222 @@
+// SPDX-License-Identifier: Apache-2.0
+// Minimal WASM bump allocator in C (no free).
+// Exports:
+//   soroban_alloc(size)                -> void*
+//   soroban_alloc_align(size, align)   -> void*
+//   soroban_alloc_init(size, init_ptr) -> struct vector*
+//       Returns a pointer to a `struct vector` (see stdlib.h),
+//       with `len` and `size` set to `size` and `data` initialized
+//       from `init_ptr` if provided.
+//   soroban_malloc(size)               -> void*
+//   soroban_realloc(ptr, new_size)     -> void*   (COPY using header)
+//   soroban_realloc_with_old(ptr, old_size, new_size) -> void* (explicit copy)
+//   soroban_free(ptr, size, align)     -> void    (no-op)
+
+#include <stdint.h>
+#include <stddef.h>
+#include "stdlib.h"
+
+#ifndef SOROBAN_PAGE_LOG2
+#define SOROBAN_PAGE_LOG2 16u // 64 KiB
+#endif
+#define SOROBAN_PAGE_SIZE (1u << SOROBAN_PAGE_LOG2)
+#define SOROBAN_MEM_INDEX 0 // wasm memory #0
+
+// clang/LLVM wasm32 intrinsics
+static inline uint32_t wasm_memory_size_pages(void)
+{
+    return (uint32_t)__builtin_wasm_memory_size(SOROBAN_MEM_INDEX);
+}
+static inline int32_t wasm_memory_grow_pages(uint32_t delta_pages)
+{
+    return (int32_t)__builtin_wasm_memory_grow(SOROBAN_MEM_INDEX, (int)delta_pages);
+}
+
+static uint32_t g_cursor = 0; // current bump (bytes)
+static uint32_t g_limit = 0;  // grown end (bytes)
+
+// We prepend a small header before each returned allocation in order to
+// remember the allocation size. This enables `realloc`-style copying even
+// though this is a bump allocator without frees.
+typedef struct
+{
+    uint32_t size; // payload size in bytes (not including this header)
+} soroban_hdr_t;
+
+static inline void *hdr_to_ptr(soroban_hdr_t *h)
+{
+    return (void *)((uintptr_t)h + sizeof(soroban_hdr_t));
+}
+static inline soroban_hdr_t *ptr_to_hdr(void *p)
+{
+    return (soroban_hdr_t *)((uintptr_t)p - sizeof(soroban_hdr_t));
+}
+
+static inline void *mem_copy(void *dst, const void *src, uint32_t n)
+{
+    // Simple, portable copy to avoid pulling in libc in freestanding mode
+    unsigned char *d = (unsigned char *)dst;
+    const unsigned char *s = (const unsigned char *)src;
+    for (uint32_t i = 0; i < n; i++)
+        d[i] = s[i];
+    return dst;
+}
+
+static inline uint32_t align_up(uint32_t addr, uint32_t align)
+{
+    if (align == 0)
+        align = 1;
+    uint32_t mask = align - 1;
+    return (addr + mask) & ~mask;
+}
+
+static inline void maybe_init(void)
+{
+    if (g_limit == 0)
+    {
+        uint32_t end = wasm_memory_size_pages() << SOROBAN_PAGE_LOG2; // bytes
+        g_cursor = end;
+        g_limit = end;
+    }
+}
+
+// grow so that `need_bytes` fits (<== need_bytes is a byte address)
+static inline int ensure_capacity(uint32_t need_bytes)
+{
+    if (need_bytes <= g_limit)
+        return 1;
+    uint32_t deficit = need_bytes - g_limit;
+    uint32_t pages = (deficit + (SOROBAN_PAGE_SIZE - 1)) >> SOROBAN_PAGE_LOG2;
+    if (wasm_memory_grow_pages(pages) < 0)
+        return 0; // OOM
+    g_limit += pages << SOROBAN_PAGE_LOG2;
+    return 1;
+}
+
+static void *alloc_impl(uint32_t bytes, uint32_t align)
+{
+    maybe_init();
+    // Ensure there is space for the header while keeping the returned pointer
+    // aligned as requested.
+    uint32_t start = align_up(g_cursor + (uint32_t)sizeof(soroban_hdr_t), align ? align : 1);
+    uint32_t end = start + bytes;
+
+    if (end > g_limit)
+    {
+        if (!ensure_capacity(end))
+            return (void *)0; // OOM
+        // retry after growth
+        start = align_up(g_cursor + (uint32_t)sizeof(soroban_hdr_t), align ? align : 1);
+        end = start + bytes;
+    }
+    g_cursor = end;
+    // Write header just before the returned pointer
+    soroban_hdr_t *hdr = (soroban_hdr_t *)(uintptr_t)(start - (uint32_t)sizeof(soroban_hdr_t));
+    hdr->size = bytes;
+    return (void *)(uintptr_t)start;
+}
+
+// -------------------- exported API --------------------
+
+__attribute__((export_name("soroban_alloc"))) void *soroban_alloc(uint32_t size)
+{
+    // default alignment 8
+    return alloc_impl(size, 8);
+}
+
+__attribute__((export_name("soroban_alloc_init"))) struct vector *soroban_alloc_init(uint32_t members,
+                                                                                     const void *init_ptr)
+{
+    // Emulate stdlib.c:vector_new() but allocate via alloc_impl.
+    // Note: here `members` is the number of bytes in the vector payload
+    // (element size assumed to be 1 for Soroban at present).
+    uint32_t size_array = members;
+
+    struct vector *v = (struct vector *)alloc_impl((uint32_t)sizeof(struct vector) + size_array, 8);
+    if (v == (struct vector *)0)
+    {
+        return (struct vector *)0;
+    }
+
+    v->len = members;
+    v->size = members;
+
+    uint8_t *data = v->data;
+
+    if (size_array)
+    {
+        if (init_ptr != (const void *)0)
+        {
+            mem_copy(data, init_ptr, size_array);
+        }
+        else
+        {
+            // zero-initialize when no initializer provided
+            for (uint32_t i = 0; i < size_array; i++)
+                data[i] = 0;
+        }
+    }
+
+    return v;
+}
+
+__attribute__((export_name("soroban_alloc_align"))) void *soroban_alloc_align(uint32_t size, uint32_t align)
+{
+    return alloc_impl(size, align);
+}
+
+__attribute__((export_name("soroban_malloc"))) void *soroban_malloc(uint32_t size)
+{
+    return alloc_impl(size, 8);
+}
+
+// Reallocate and copy previous contents. Since we store a small header in
+// front of each allocation, we can determine the old size here and copy the
+// minimum of old and new sizes.
+__attribute__((export_name("soroban_realloc"))) void *soroban_realloc(void *old_ptr, uint32_t new_size)
+{
+    if (old_ptr == (void *)0)
+    {
+        return alloc_impl(new_size, 8);
+    }
+
+    // Determine old size from the header placed before the allocation
+    soroban_hdr_t *old_hdr = ptr_to_hdr(old_ptr);
+    uint32_t old_size = old_hdr->size;
+
+    void *new_ptr = alloc_impl(new_size, 8);
+    if (new_ptr == (void *)0)
+        return (void *)0; // OOM
+
+    uint32_t copy = old_size < new_size ? old_size : new_size;
+    if (copy)
+        mem_copy(new_ptr, old_ptr, copy);
+    return new_ptr;
+}
+
+// Variant that accepts the old size explicitly. Useful when the caller
+// already knows the previous allocation size and wants to avoid relying on
+// the header (or for interop with older allocations).
+__attribute__((export_name("soroban_realloc_with_old"))) void *soroban_realloc_with_old(void *old_ptr,
+                                                                                        uint32_t old_size,
+                                                                                        uint32_t new_size)
+{
+    if (old_ptr == (void *)0)
+    {
+        return alloc_impl(new_size, 8);
+    }
+    void *new_ptr = alloc_impl(new_size, 8);
+    if (new_ptr == (void *)0)
+        return (void *)0; // OOM
+    uint32_t copy = old_size < new_size ? old_size : new_size;
+    if (copy)
+        mem_copy(new_ptr, old_ptr, copy);
+    return new_ptr;
+}
+
+__attribute__((export_name("soroban_free"))) void soroban_free(void *_ptr, uint32_t _size, uint32_t _align)
+{
+    (void)_ptr;
+    (void)_size;
+    (void)_align; // bump allocator: no-op
+}

+ 1 - 2
tests/soroban.rs

@@ -1,8 +1,6 @@
 // SPDX-License-Identifier: Apache-2.0
 
 #[cfg(feature = "soroban")]
-pub mod soroban_testcases;
-
 use solang::codegen::Options;
 use solang::file_resolver::FileResolver;
 use solang::sema::ast::Namespace;
@@ -11,6 +9,7 @@ use solang::{compile, Target};
 use soroban_sdk::testutils::Logs;
 use soroban_sdk::{vec, Address, ConstructorArgs, Env, Symbol, Val};
 use std::ffi::OsStr;
+pub mod soroban_testcases;
 
 // TODO: register accounts, related balances, events, etc.
 pub struct SorobanEnv {

+ 80 - 0
tests/soroban_testcases/alloc.rs

@@ -0,0 +1,80 @@
+// SPDX-License-Identifier: Apache-2.0
+
+use crate::build_solidity;
+use soroban_sdk::{IntoVal, Val};
+
+#[test]
+fn arrays_basic_ops_test() {
+    let runtime = build_solidity(
+        r#"
+        contract array {
+            function push_pop() public returns (uint64) {
+                uint64[] mylist;
+
+                mylist.push(5);
+                mylist.push(10);
+                mylist.pop();
+
+                uint64 len = mylist.length;
+
+                return len + mylist[0];
+            }
+
+            function loop() public returns (uint64) {
+                uint64[] mylist;
+                uint64 sum = 0;
+
+                mylist.push(5);
+                mylist.push(10);
+                mylist.push(15);
+
+                for (uint64 i = 0; i < mylist.length; i++) {
+                    sum += mylist[i];
+                }
+
+                return sum;
+            }
+
+            function random_access(uint64 index) public returns (uint64) {
+                uint64[] mylist;
+                uint64 sum = 0;
+
+                mylist.push(5);
+                mylist.push(10);
+                mylist.push(15);
+
+                sum += mylist[index];
+                sum += mylist[index + 1];
+
+                return sum;
+            }
+        }
+        "#,
+        |_| {},
+    );
+
+    let addr = runtime.contracts.last().unwrap();
+
+    // 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);
+    assert!(expected.shallow_eq(&res));
+
+    // loop(): 5 + 10 + 15 = 30
+    let expected: Val = 30_u64.into_val(&runtime.env);
+    let res = runtime.invoke_contract(addr, "loop", vec![]);
+    assert!(expected.shallow_eq(&res));
+
+    // random_access(0): mylist[0] + mylist[1] = 5 + 10 = 15
+    let expected: Val = 15_u64.into_val(&runtime.env);
+    let args = vec![0_u64.into_val(&runtime.env)];
+    let res = runtime.invoke_contract(addr, "random_access", args);
+    assert!(expected.shallow_eq(&res));
+
+    // random_access(1): mylist[1] + mylist[2] = 10 + 15 = 25
+    let expected: Val = 25_u64.into_val(&runtime.env);
+    let args = vec![1_u64.into_val(&runtime.env)];
+    let res = runtime.invoke_contract(addr, "random_access", args);
+    assert!(expected.shallow_eq(&res));
+}

+ 1 - 0
tests/soroban_testcases/mod.rs

@@ -1,4 +1,5 @@
 // SPDX-License-Identifier: Apache-2.0
+mod alloc;
 mod auth;
 mod constructor;
 mod cross_contract_calls;

+ 2 - 0
tests/soroban_testcases/print.rs

@@ -23,6 +23,8 @@ fn log_runtime_error() {
 
     let logs = src.invoke_contract_expect_error(addr, "decrement", vec![]);
 
+    println!("logs {logs:?}");
+
     assert!(logs[0].contains("runtime_error: math overflow in test.sol:5:17-27"));
 }