From 289e862fae56901b9e7fb90d5f29fa8d573111a5 Mon Sep 17 00:00:00 2001 From: Pierre Augier Date: Fri, 11 Sep 2026 11:41:33 +0200 Subject: [PATCH] testing: SPyTuple in Python --- spy/tests/compiler/test_struct.py | 17 ++++++++++++++++- spy/tests/compiler/test_tuple.py | 29 +++++++++++++++++++++++++++++ spy/tests/wasm_wrapper.py | 15 ++++++++++----- spy/vm/struct.py | 22 ++++++++++++++++++++++ 4 files changed, 77 insertions(+), 6 deletions(-) diff --git a/spy/tests/compiler/test_struct.py b/spy/tests/compiler/test_struct.py index fe86ed68e..920811f9b 100644 --- a/spy/tests/compiler/test_struct.py +++ b/spy/tests/compiler/test_struct.py @@ -8,7 +8,7 @@ from spy.vm.modules.unsafe import UNSAFE from spy.vm.object import W_Type from spy.vm.registry import ModuleRegistry -from spy.vm.struct import UnwrappedStruct +from spy.vm.struct import SPyTuple, UnwrappedStruct def test_UnwrappedStruct(): @@ -22,6 +22,21 @@ def test_UnwrappedStruct(): assert us1 == (1, 2) +def test_SPyTuple(): + tup = SPyTuple(FQN("_tuple::tuple[i32, i32]::_tup"), {"_item0": 4, "_item1": 8}) + assert tup == (4, 8) + assert len(tup) == 2 + assert tup[0] == 4 + assert tup[1] == 8 + assert list(tup) == [4, 8] + a, b = tup + assert (a, b) == (4, 8) + assert repr(tup) == "SPyTuple([4, 8])" + # attribute access is still available, like any other UnwrappedStruct + assert tup._item0 == 4 + assert tup._item1 == 8 + + class TestStructOnStack(CompilerTest): """ Test for structs allocated on the stack, passed around by value as diff --git a/spy/tests/compiler/test_tuple.py b/spy/tests/compiler/test_tuple.py index 862ac4ab4..1eff78e3e 100644 --- a/spy/tests/compiler/test_tuple.py +++ b/spy/tests/compiler/test_tuple.py @@ -71,6 +71,35 @@ def foo(a: i32, b: i32) -> tuple[i32, i32]: tup = mod.foo(3, 4) assert tup == (3, 4) + def test_testing_return_tuple(self): + # tuples are returned to the *Python* test + mod = self.compile(""" + def foo() -> tuple[i32, i32, i32]: + return 1, 2, 3 + + def nested() -> tuple[i32, tuple[i32, i32]]: + return 1, (2, 3) + + def mixed_type() -> tuple[i32, f64, bool, str]: + return 1, 2.5, True, "hello" + """) + a, b, c = mod.foo() + assert a == 1 + assert b == 2 + + tup = mod.foo() + assert len(tup) == 3 + assert tup[0] == 1 + assert tup[2] == 3 + + tup = mod.nested() + assert tup == (1, (2, 3)) + a, (b, c) = tup + assert (a, b, c) == (1, 2, 3) + + tup = mod.mixed_type() + assert tup == (1, 2.5, True, "hello") + def test_unpacking_wrong_number(self): src = """ def make_tuple() -> tuple[int, int]: diff --git a/spy/tests/wasm_wrapper.py b/spy/tests/wasm_wrapper.py index 700a97141..4d72f736b 100644 --- a/spy/tests/wasm_wrapper.py +++ b/spy/tests/wasm_wrapper.py @@ -16,7 +16,7 @@ from spy.vm.modules.unsafe.ptr import W_PtrType from spy.vm.object import W_Type from spy.vm.str import ll_str_new -from spy.vm.struct import UnwrappedStruct, W_StructType +from spy.vm.struct import SPyTuple, UnwrappedStruct, W_StructType from spy.vm.vm import SPyVM @@ -189,9 +189,12 @@ def to_py_result(self, w_T: W_Type, res: Any) -> Any: return WasmPtr(addr, length) elif isinstance(w_T, W_StructType): # when you return struct-by-val from C, wasmtime automatically - # converts them into a list, flattening nested structs - assert isinstance(res, list) - pyres = unflatten_struct(self.ll, w_T, res) + # converts them into a list, flattening nested structs. However, + # for a struct with a single flat field, wasmtime returns a bare + # scalar instead of a one-element list. + if not isinstance(res, list): + res = [res] + pyres = unflatten_struct(self.vm, self.ll, w_T, res) if w_T.fqn == FQN( "_list::list[i32]::_ListImpl" ): # reading list[i32] for tests @@ -308,7 +311,7 @@ def __call__(self, *py_args: Any, unwrap: bool = True) -> Any: def unflatten_struct( - ll: LLSPyInstance, w_T: W_StructType, flat_values: list[Any] + vm: SPyVM, ll: LLSPyInstance, w_T: W_StructType, flat_values: list[Any] ) -> UnwrappedStruct: """ Unflatten a struct from a flat list of values. @@ -354,6 +357,8 @@ def unflatten(w_T: W_StructType, start_idx: int) -> tuple[UnwrappedStruct, int]: content[w_field.name] = flat_values[idx] idx += 1 + if vm.is_tuple_type(w_T): + return SPyTuple(w_T.fqn, content), idx return UnwrappedStruct(w_T.fqn, content), idx result, consumed = unflatten(w_T, 0) diff --git a/spy/vm/struct.py b/spy/vm/struct.py index 81bb5f558..7e3dbecdf 100644 --- a/spy/vm/struct.py +++ b/spy/vm/struct.py @@ -338,6 +338,8 @@ def spy_unwrap(self, vm: "SPyVM") -> Any: fqn = self.w_structtype.fqn fields = {key: w_obj.spy_unwrap(vm) for key, w_obj in self.values_w.items()} + if vm.is_tuple_type(self.w_structtype): + return SPyTuple(fqn, fields) return UnwrappedStruct(fqn, fields) def __repr__(self) -> str: @@ -431,6 +433,26 @@ def __repr__(self) -> str: return f"" +class SPyTuple(UnwrappedStruct): + """ + Return value of vm.unwrap(w_some_tuple), where w_some_tuple is an + instance of the stdlib `tuple[T1, T2, ...]` (which is implemented as a + struct with fields `_item0`, `_item1`, ...). Purely a testing convenience. + """ + + def __len__(self) -> int: + return len(self._content) + + def __iter__(self) -> Any: + return iter(self._content.values()) + + def __getitem__(self, i: Any) -> Any: + return tuple(self._content.values())[i] + + def __repr__(self) -> str: + return f"SPyTuple({list(self._content.values())!r})" + + def unwrap_list(vm: "SPyVM", w_list: W_Object) -> list[Any]: """ Only useful in tests