diff --git a/spy/backend/c/cstructwriter.py b/spy/backend/c/cstructwriter.py index 81cf4b545..d729cfa35 100644 --- a/spy/backend/c/cstructwriter.py +++ b/spy/backend/c/cstructwriter.py @@ -7,7 +7,7 @@ from spy.backend.c.context import C_Type, Context from spy.fqn import FQN from spy.textbuilder import TextBuilder -from spy.vm.modules.unsafe.misc import contains_gc_ptr +from spy.vm.modules.unsafe.misc import alignof, contains_gc_ptr from spy.vm.modules.unsafe.ptr import W_PtrType, W_RefType from spy.vm.object import W_Type from spy.vm.struct import W_StructType @@ -171,6 +171,12 @@ def emit_PtrType(self, fqn: FQN, w_ptrtype: W_PtrType) -> None: self.tbh_fwdecl.wl( f"// {c_ptrtype}: skip as it's already pre-declared by libspy" ) + # The struct typedef and SPY_PTR_FUNCTIONS are hand-written in + # libspy for these types, but the per-field unaligned helpers + # are NOT (they depend on the field layout of the *item* type, + # which the C backend, not libspy, knows about) -- so those + # still need to be emitted here if needed. + self._emit_ptr_field_helpers(w_ptrtype) return self.tbh_fwdecl.wb(f""" @@ -196,12 +202,54 @@ def emit_PtrType(self, fqn: FQN, w_ptrtype: W_PtrType) -> None: # to scan it. See spy/libspy/include/spy/unsafe.h. alloc_func = "gc_alloc_pointerless" + alignment = w_ptrtype.alignment self.tbh_ptrs_def.wb(f""" - SPY_PTR_FUNCTIONS({alloc_func}, {c_ptrtype}, {c_itemT}); + SPY_PTR_FUNCTIONS({alloc_func}, {c_ptrtype}, {c_itemT}, {alignment}); #define {c_ptrtype}$NULL (({c_ptrtype}){{0}}) """) self.tbh_ptrs_def.wl() + # Emit per-field unaligned helpers if the ptr may be under-aligned + # relative to one of the item struct's fields. + self._emit_ptr_field_helpers(w_ptrtype) + + def _emit_ptr_field_helpers(self, w_ptrtype: W_PtrType) -> None: + """ + Emit unaligned load/store helpers for struct fields accessed + through an under-aligned ptr (see fmt_ptr_getfield/fmt_ptr_setfield + in cwriter.py, which decide when to call these instead of a plain + typed field access). + """ + w_itemT = w_ptrtype.w_itemT + if not isinstance(w_itemT, W_StructType) or not w_itemT.is_defined(): + return + + c_ptrtype = C_Type(w_ptrtype.fqn.c_name) + ptr_align = w_ptrtype.alignment + + for w_field in w_itemT.iterfields_w(): + field_align = alignof(w_field.w_T) + if ptr_align >= field_align: + continue + + c_fieldtype = self.ctx.w2c(w_field.w_T) + c_fieldname = w_field.name + + self.tbh_ptrs_def.wb(f""" + static inline {c_fieldtype} {c_ptrtype}$getfield_{c_fieldname}_unaligned( + {c_ptrtype} p) {{ + {c_fieldtype} _tmp; + __builtin_memcpy(&_tmp, (const char *)p.p + {w_field.offset}, sizeof({c_fieldtype})); + return _tmp; + }} + + static inline void {c_ptrtype}$setfield_{c_fieldname}_unaligned( + {c_ptrtype} p, {c_fieldtype} v) {{ + __builtin_memcpy((char *)p.p + {w_field.offset}, &v, sizeof({c_fieldtype})); + }} + """) + self.tbh_ptrs_def.wl() + def emit_RefType(self, fqn: FQN, w_reftype: W_RefType) -> None: w_ptrtype = w_reftype.as_ptrtype(self.ctx.vm) c_reftype = C_Type(w_reftype.fqn.c_name) diff --git a/spy/backend/c/cwriter.py b/spy/backend/c/cwriter.py index dcb17b390..82322db39 100644 --- a/spy/backend/c/cwriter.py +++ b/spy/backend/c/cwriter.py @@ -14,7 +14,9 @@ from spy.vm.function import W_ASTFunc, W_Func from spy.vm.irtag import IRTag from spy.vm.modules.posix import W__FILE -from spy.vm.modules.unsafe.ptr import W_Ptr +from spy.vm.modules.unsafe.misc import alignof, sizeof +from spy.vm.modules.unsafe.ptr import W_Ptr, W_PtrType +from spy.vm.struct import W_StructType if TYPE_CHECKING: from spy.backend.c.cmodwriter import CModuleWriter @@ -590,6 +592,15 @@ def fmt_expr_Call(self, call: ast.Call) -> C.Expr: # we handle ptr.deref explicitly for extra clarity return self.fmt_generic_call(fqn, call) + elif irtag.tag == "ptr.weaken_align": + return self.fmt_ptr_weaken_align(fqn, call) + + elif irtag.tag == "unsafe.cast": + return self.fmt_cast(fqn, call) + + elif irtag.tag == "unsafe.align_cast": + return self.fmt_align_cast(fqn, call, irtag) + elif irtag.tag in ("ptr.getitem", "ptr.store"): # see unsafe/ptr.py::w_GETITEM and w_SETITEM there, we insert an # extra "w_loc" argument, which is not needed by the C backend @@ -632,11 +643,114 @@ def fmt_struct_getfield(self, fqn: FQN, call: ast.Call, irtag: IRTag) -> C.Expr: name = irtag.data["name"] return C.Dot(c_struct, name) + def fmt_ptr_weaken_align(self, fqn: FQN, call: ast.Call) -> C.Expr: + """ + gc_ptr[T,N] -> gc_ptr[T,M] weakening conversion. Both types have + byte-identical C layout ({T *p; length}), so this is just a + relabeling. + """ + assert len(call.args) == 1 + w_srcT = call.args[0].w_T + assert w_srcT is not None + c_src = self.fmt_expr(call.args[0]) + c_srctype = self.ctx.w2c(w_srcT) + c_targettype = self.ctx.c_restype_by_fqn(fqn) + c_p = C.Literal(f"({c_src}).p") + c_length = C.Call(f"{c_srctype}_get_length", [c_src]) + return C.Call(f"{c_targettype}_from_raw", [c_p, c_length]) + + def fmt_cast(self, fqn: FQN, call: ast.Call) -> C.Expr: + """ + cast[DstItemT](ptr) -> ptr with a new item type, same address and alignment. + """ + assert len(call.args) == 1 + w_srcT = call.args[0].w_T + assert isinstance(w_srcT, W_PtrType) + c_src = self.fmt_expr(call.args[0]) + c_srctype = self.ctx.w2c(w_srcT) + c_targettype = self.ctx.c_restype_by_fqn(fqn) + + w_func = self.ctx.vm.lookup_global(fqn) + assert isinstance(w_func, W_Func) + w_dstT = w_func.w_functype.w_restype + assert isinstance(w_dstT, W_PtrType) + + src_size = sizeof(w_srcT.w_itemT) + dst_size = sizeof(w_dstT.w_itemT) + + c_itemtype = self.ctx.w2c(w_dstT.w_itemT) + c_p = C.Literal(f"({c_itemtype} *)({c_src}).p") + c_old_length = C.Call(f"{c_srctype}_get_length", [c_src]) + c_new_length = C.Literal(f"(({c_old_length}) * {src_size} / {dst_size})") + return C.Call(f"{c_targettype}_from_raw", [c_p, c_new_length]) + + def fmt_align_cast(self, fqn: FQN, call: ast.Call, irtag: IRTag) -> C.Expr: + """ + align_cast[N](ptr) -> ptr with a new alignment, same address and + item type. N is baked into `fqn` (see fmt_cast above for why + `call.args` has just the ptr). + + Weakening (N <= old_alignment) is just a relabeling, same as + ptr.weaken_align. Strengthening additionally needs a DEBUG-mode + runtime check; we emit it via a small static-inline helper + (spy_check_align, declared in unsafe.h) rather than an inline GCC + statement expression, so it composes normally with the rest of the + C AST and doesn't rely on a non-standard extension. + """ + assert len(call.args) == 1 + w_srcT = call.args[0].w_T + assert isinstance(w_srcT, W_PtrType) + c_src = self.fmt_expr(call.args[0]) + c_srctype = self.ctx.w2c(w_srcT) + c_targettype = self.ctx.c_restype_by_fqn(fqn) + + new_alignment = irtag.data["new_alignment"] + old_alignment = irtag.data["old_alignment"] + + c_p = C.Literal(f"({c_src}).p") + c_length = C.Call(f"{c_srctype}_get_length", [c_src]) + + if new_alignment > old_alignment: + # spy_check_align(p, N) panics (in DEBUG builds) if `p` is not + # aligned to N; it's a no-op in RELEASE builds. + c_checked_p = C.Call( + "spy_check_align", [c_p, C.Literal(str(new_alignment))] + ) + return C.Call(f"{c_targettype}_from_raw", [c_checked_p, c_length]) + else: + return C.Call(f"{c_targettype}_from_raw", [c_p, c_length]) + + def _is_under_aligned_field(self, w_ptr: object, attr: str) -> bool: + """ + True if w_ptr is a W_PtrType pointing to a defined struct, and + `attr` names a field whose natural alignment exceeds the ptr's + declared alignment -- i.e. a plain typed access to it would be + undefined behavior and must instead go through the + $getfield_*_unaligned / $setfield_*_unaligned helpers emitted by + CStructWriter._emit_ptr_field_helpers. + """ + if not isinstance(w_ptr, W_PtrType): + return False + w_itemT = w_ptr.w_itemT + if not isinstance(w_itemT, W_StructType) or not w_itemT.is_defined(): + return False + for w_field in w_itemT.iterfields_w(): + if w_field.name == attr: + return w_ptr.alignment < alignof(w_field.w_T) + return False + def fmt_ptr_getfield(self, fqn: FQN, call: ast.Call, irtag: IRTag) -> C.Expr: assert isinstance(call.args[1], ast.StrLiteral) c_ptr = self.fmt_expr(call.args[0]) attr = call.args[1].value offset = call.args[2] # ignored + + w_ptr = call.args[0].w_T + if self._is_under_aligned_field(w_ptr, attr): + assert w_ptr is not None + c_ptrtype = self.ctx.w2c(w_ptr) + return C.Call(f"{c_ptrtype}$getfield_{attr}_unaligned", [c_ptr]) + c_field = C.PtrField(c_ptr, attr) if irtag.data["by"] == "byref": c_restype = self.ctx.c_restype_by_fqn(fqn) @@ -649,8 +763,15 @@ def fmt_ptr_setfield(self, fqn: FQN, call: ast.Call) -> C.Expr: c_ptr = self.fmt_expr(call.args[0]) attr = call.args[1].value offset = call.args[2] # ignored - c_lval = C.PtrField(c_ptr, attr) c_rval = self.fmt_expr(call.args[3]) + + w_ptr = call.args[0].w_T + if self._is_under_aligned_field(w_ptr, attr): + assert w_ptr is not None + c_ptrtype = self.ctx.w2c(w_ptr) + return C.Call(f"{c_ptrtype}$setfield_{attr}_unaligned", [c_ptr, c_rval]) + + c_lval = C.PtrField(c_ptr, attr) return C.BinOp("=", c_lval, c_rval) def fmt_memop(self, fqn: FQN, call: ast.Call, irtag: IRTag) -> C.Expr: diff --git a/spy/libspy/include/spy/bytes.h b/spy/libspy/include/spy/bytes.h index 82fb28657..6d96337da 100644 --- a/spy/libspy/include/spy/bytes.h +++ b/spy/libspy/include/spy/bytes.h @@ -49,7 +49,7 @@ typedef struct spy_unsafe$gc_ptr___bytes$BytesObject { #endif } spy_unsafe$gc_ptr___bytes$BytesObject; -SPY_PTR_FUNCTIONS(gc_alloc, spy_unsafe$gc_ptr___bytes$BytesObject, spy_BytesObject) +SPY_PTR_FUNCTIONS(gc_alloc, spy_unsafe$gc_ptr___bytes$BytesObject, spy_BytesObject, 4) #define spy_unsafe$gc_ptr___bytes$BytesObject$NULL \ ((spy_unsafe$gc_ptr___bytes$BytesObject){0}) diff --git a/spy/libspy/include/spy/str.h b/spy/libspy/include/spy/str.h index a6f303fba..26e4e5342 100644 --- a/spy/libspy/include/spy/str.h +++ b/spy/libspy/include/spy/str.h @@ -43,7 +43,7 @@ typedef struct spy_unsafe$gc_ptr___str$StrObject { #endif } spy_unsafe$gc_ptr___str$StrObject; -SPY_PTR_FUNCTIONS(gc_alloc, spy_unsafe$gc_ptr___str$StrObject, spy_StrObject) +SPY_PTR_FUNCTIONS(gc_alloc, spy_unsafe$gc_ptr___str$StrObject, spy_StrObject, 4) #define spy_unsafe$gc_ptr___str$StrObject$NULL ((spy_unsafe$gc_ptr___str$StrObject){0}) // short alias for manual use diff --git a/spy/libspy/include/spy/unsafe.h b/spy/libspy/include/spy/unsafe.h index d1d76f93c..fe649d654 100644 --- a/spy/libspy/include/spy/unsafe.h +++ b/spy/libspy/include/spy/unsafe.h @@ -14,6 +14,69 @@ void WASM_EXPORT(_spy_memmove)(void *dst, void *src, size_t n); void WASM_EXPORT(_spy_memset)(void *dst, int value, size_t n); int32_t WASM_EXPORT(_spy_memcmp)(void *a, void *b, size_t n); +// Aligned allocation wrappers used by the interp (vm.ll.call) path. +// The C backend's $alloc macro calls spy_alloc_aligned_impl directly. +void *WASM_EXPORT(spy_raw_alloc_aligned)(size_t size, size_t alignment); +void *WASM_EXPORT(spy_nogc_alloc_aligned)(size_t size, size_t alignment); + +// The base alignment that the underlying allocators (malloc, GC_MALLOC, +// GC_MALLOC_ATOMIC) already guarantee. When a ptr type requests an +// alignment <= SPY_BASE_ALIGNMENT the $alloc fast path can skip the +// over-allocation / pointer-adjustment dance entirely. +// +// On wasm32 / wasm64, linear-memory allocators return 8-byte-aligned +// pointers. On native 64-bit targets, malloc and GC_MALLOC guarantee +// 16-byte alignment (alignof(max_align_t)). We use the larger value so +// that the fast path is taken whenever alignment <= 16, which covers +// every natural SPy type alignment (the largest is 8 for f64/i64). +#if defined(SPY_TARGET_NATIVE) && defined(__LP64__) +# define SPY_BASE_ALIGNMENT 16 +#else +# define SPY_BASE_ALIGNMENT 8 +#endif + +// Allocate `n` bytes with at least `alignment`-byte alignment, using the +// given `alloc_func` (one of spy_raw_alloc, spy_nogc_alloc, or the +// GC_MALLOC-based helpers). When `alignment <= SPY_BASE_ALIGNMENT` the +// allocator already satisfies the request, so we delegate directly with +// no over-allocation, rounding, or base-pointer discarding. Otherwise we +// over-allocate by `alignment` bytes, round the raw pointer up to the +// next aligned address, and return that. +// +// NOTE: this means the original base pointer is *lost* — the returned +// pointer cannot be passed to free(). This is fine for SPy's GC-managed +// and raw_alloc (never freed) allocators, but would be a problem for a +// general-purpose allocator that needs to reclaim memory. The over- +// allocation "wastes" at most (alignment - 1) bytes. +static inline void * +spy_alloc_aligned_impl(size_t n, size_t alignment, void *(*alloc_func)(size_t)) { + if (alignment <= SPY_BASE_ALIGNMENT) { + // the allocator already guarantees this alignment. + return alloc_func(n); + } + char *raw = (char *)alloc_func(n + alignment); + uintptr_t a = ((uintptr_t)raw + alignment - 1) & ~(alignment - 1); + return (void *)a; +} + +// Check that `p` is aligned to `alignment` bytes, and return `p` unchanged. +// Used by align_cast[N](ptr) when N strengthens the ptr's alignment claim +// (N > the ptr's current alignment): in SPY_DEBUG builds this panics if +// the check fails, catching a false claim before it can cause misaligned +// accesses further down the line; in RELEASE builds it's a no-op and the +// caller's claim is trusted, so the compiler is free to optimize the call +// away entirely (e.g. when `alignment` is a compile-time constant already +// known to hold). +static inline void * +spy_check_align(void *p, size_t alignment) { +#ifdef SPY_DEBUG + if ((uintptr_t)p % alignment != 0) { + spy_panic("PanicError", "align_cast: address not aligned", __FILE__, __LINE__); + } +#endif + return p; +} + #ifdef SPY_GC_NONE # define spy_gc_alloc(size) spy_nogc_alloc(size) # define spy_gc_alloc_pointerless(size) spy_nogc_alloc(size) @@ -33,6 +96,32 @@ spy_gc_alloc_pointerless_bdwgc(size_t size) { # error "no GC selected" #endif +// spy_gc_alloc and spy_gc_alloc_pointerless (defined just above) are +// function-like MACROS, not real functions: they only expand when +// immediately followed by "(...)". spy_alloc_aligned_impl needs an +// addressable function pointer, so a bare "spy_gc_alloc" (with no call +// parens) does NOT expand and fails to compile. These thin static-inline +// wrappers give us addressable symbols that simply forward to the macros. +static inline void * +spy_gc_alloc_fn(size_t size) { + return spy_gc_alloc(size); +} + +static inline void * +spy_gc_alloc_pointerless_fn(size_t size) { + return spy_gc_alloc_pointerless(size); +} + +// Map an ALLOC_FUNC token (as used by SPY_PTR_FUNCTIONS: raw_alloc, +// gc_alloc, gc_alloc_pointerless) to the actual function symbol that can be +// passed as a function pointer to spy_alloc_aligned_impl. raw_alloc is +// already a real function (spy_raw_alloc), so it maps to itself; gc_alloc +// and gc_alloc_pointerless are macros, so they map to the _fn wrappers +// above instead. +#define _SPY_ALLOC_FN_raw_alloc spy_raw_alloc +#define _SPY_ALLOC_FN_gc_alloc spy_gc_alloc_fn +#define _SPY_ALLOC_FN_gc_alloc_pointerless spy_gc_alloc_pointerless_fn + /* Define the struct and accessor functions to represent a managed pointer to type T. @@ -44,8 +133,8 @@ spy_gc_alloc_pointerless_bdwgc(size_t size) { #endif } Ptr_T; - SPY_PTR_FUNCTIONS(raw_alloc, Ptr_T, T) defines all the accessor functions such as - Ptr_T$alloc, Ptr_T$load, etc. + SPY_PTR_FUNCTIONS(raw_alloc, Ptr_T, T, ALIGNMENT) defines all the accessor + functions such as Ptr_T$alloc, Ptr_T$load, etc. In SPY_RELEASE mode, a managed pointer is just a wrapper around an unmanaged C pointer, but in SPY_DEBUG it also contains the length of the @@ -57,32 +146,77 @@ spy_gc_alloc_pointerless_bdwgc(size_t size) { - "gc_alloc" (GC_MALLOC: zeroed, scanned) - "gc_alloc_pointerless" (GC_MALLOC_ATOMIC: not zeroed, not scanned; only for pointer-free T) + + ALIGNMENT is the requested alignment in bytes for the allocated + block. When it exceeds SPY_BASE_ALIGNMENT, $alloc over-allocates and + adjusts the pointer via spy_alloc_aligned_impl. */ +/* Unaligned access helpers. + * + * When a ptr type declares an alignment strictly less than the natural + * alignment of its item type T (e.g. gc_ptr[i32, 1], where + * alignof(i32) == 4), a plain typed dereference/store is undefined + * behavior. These helpers route the access through __builtin_memcpy, + * which compilers lower to the most efficient unaligned access for the + * target, whenever ALIGNMENT < alignof(T). + * + * ALIGNMENT and _Alignof(T) are compile-time constants, so the branch + * is folded away at compile time: there is zero runtime overhead in the + * (overwhelmingly common) case where the ptr is naturally aligned. + */ +#define _SPY_PTR_LOAD(T, ALIGNMENT, addr) \ + ((ALIGNMENT) >= _Alignof(T) ? *(addr) : ({ \ + T _tmp; \ + __builtin_memcpy(&_tmp, (const char *)(addr), sizeof(T)); \ + _tmp; \ + })) + +#define _SPY_PTR_STORE(T, ALIGNMENT, addr, rval) \ + do { \ + if ((ALIGNMENT) >= _Alignof(T)) { \ + *(addr) = (rval); \ + } else { \ + T _tmp = (rval); \ + __builtin_memcpy((char *)(addr), &_tmp, sizeof(T)); \ + } \ + } while (0) + #ifdef SPY_DEBUG # define SPY_PTR_FUNCTIONS _SPY_PTR_FUNCTIONS_CHECKED #else # define SPY_PTR_FUNCTIONS _SPY_PTR_FUNCTIONS_UNCHECKED #endif -#define _SPY_PTR_FUNCTIONS_UNCHECKED(ALLOC_FUNC, PTR, T) \ +#define _SPY_PTR_FUNCTIONS_UNCHECKED(ALLOC_FUNC, PTR, T, ALIGNMENT) \ static inline PTR PTR##_from_addr(T *p) { \ return (PTR){p}; \ } \ + static inline ptrdiff_t PTR##_get_length(PTR p) { \ + (void)p; \ + return 0; \ + } \ + static inline PTR PTR##_from_raw(T *p, ptrdiff_t length) { \ + (void)length; \ + return (PTR){p}; \ + } \ static inline PTR PTR##$alloc(size_t n) { \ - return (PTR){(T*)spy_##ALLOC_FUNC(sizeof(T) * n)}; \ + T *p = (T *)spy_alloc_aligned_impl( \ + sizeof(T) * n, (ALIGNMENT), _SPY_ALLOC_FN_##ALLOC_FUNC \ + ); \ + return (PTR){p}; \ } \ static inline T PTR##$deref(PTR p) { \ - return *(p.p); \ + return _SPY_PTR_LOAD(T, ALIGNMENT, p.p); \ } \ static inline T PTR##$getitem_byval(PTR p, ptrdiff_t i) { \ - return p.p[i]; \ + return _SPY_PTR_LOAD(T, ALIGNMENT, p.p + i); \ } \ static inline PTR PTR##$getitem_byref(PTR p, ptrdiff_t i) { \ return PTR##_from_addr(p.p + i); \ } \ static inline void PTR##$store(PTR p, ptrdiff_t i, T v) { \ - p.p[i] = v; \ + _SPY_PTR_STORE(T, ALIGNMENT, p.p + i, v); \ } \ static inline bool PTR##$__eq__(PTR p0, PTR p1) { \ return p0.p == p1.p; \ @@ -92,17 +226,32 @@ spy_gc_alloc_pointerless_bdwgc(size_t size) { } \ static inline bool PTR##$to_bool(PTR p) { \ return p.p; \ + } \ + static inline int32_t PTR##$to_addr(PTR p) { \ + /* NOTE: truncates to 32 bits. Only meaningful on wasm32-like \ + targets where addresses actually fit in 32 bits. See the \ + comment on W_MemLoc.addr in spy/vm/modules/unsafe/ptr.py. */ \ + return (int32_t)(uintptr_t)p.p; \ } -#define _SPY_PTR_FUNCTIONS_CHECKED(ALLOC_FUNC, PTR, T) \ +#define _SPY_PTR_FUNCTIONS_CHECKED(ALLOC_FUNC, PTR, T, ALIGNMENT) \ static inline PTR PTR##_from_addr(T *p) { \ return (PTR){p, 1}; \ } \ + static inline ptrdiff_t PTR##_get_length(PTR p) { \ + return p.length; \ + } \ + static inline PTR PTR##_from_raw(T *p, ptrdiff_t length) { \ + return (PTR){p, length}; \ + } \ static inline PTR PTR##$alloc(size_t n) { \ - return (PTR){(T*)spy_##ALLOC_FUNC(sizeof(T) * n), (ptrdiff_t) n}; \ + T *p = (T *)spy_alloc_aligned_impl( \ + sizeof(T) * n, (ALIGNMENT), _SPY_ALLOC_FN_##ALLOC_FUNC \ + ); \ + return (PTR){p, (ptrdiff_t)n}; \ } \ static inline T PTR##$deref(PTR p) { \ - return *(p.p); \ + return _SPY_PTR_LOAD(T, ALIGNMENT, p.p); \ } \ static inline T PTR##$getitem_byval(PTR p, ptrdiff_t i) { \ if (p.p == NULL) \ @@ -111,7 +260,7 @@ spy_gc_alloc_pointerless_bdwgc(size_t size) { ); \ if (i < 0 || i >= p.length) \ spy_panic("PanicError", "ptr_getitem out of bounds", __FILE__, __LINE__); \ - return p.p[i]; \ + return _SPY_PTR_LOAD(T, ALIGNMENT, p.p + i); \ } \ static inline PTR PTR##$getitem_byref(PTR p, ptrdiff_t i) { \ if (p.p == NULL) \ @@ -129,7 +278,7 @@ spy_gc_alloc_pointerless_bdwgc(size_t size) { ); \ if (i < 0 || i >= p.length) \ spy_panic("PanicError", "ptr_store out of bounds", __FILE__, __LINE__); \ - p.p[i] = v; \ + _SPY_PTR_STORE(T, ALIGNMENT, p.p + i, v); \ } \ static inline bool PTR##$__eq__(PTR p0, PTR p1) { \ return p0.p == p1.p && p0.length == p1.length; \ @@ -139,6 +288,12 @@ spy_gc_alloc_pointerless_bdwgc(size_t size) { } \ static inline bool PTR##$to_bool(PTR p) { \ return p.p; \ + } \ + static inline int32_t PTR##$to_addr(PTR p) { \ + /* NOTE: truncates to 32 bits. Only meaningful on wasm32-like \ + targets where addresses actually fit in 32 bits. See the \ + comment on W_MemLoc.addr in spy/vm/modules/unsafe/ptr.py. */ \ + return (int32_t)(uintptr_t)p.p; \ } /* gc_ptr[u8] is predeclared here, see also cstructwriter.py:emit_PtrType. @@ -150,7 +305,7 @@ typedef struct spy_unsafe$gc_ptr__builtins$u8 { #endif } spy_unsafe$gc_ptr__builtins$u8; -SPY_PTR_FUNCTIONS(gc_alloc, spy_unsafe$gc_ptr__builtins$u8, uint8_t) +SPY_PTR_FUNCTIONS(gc_alloc, spy_unsafe$gc_ptr__builtins$u8, uint8_t, 1) #define spy_unsafe$gc_ptr__builtins$u8$NULL ((spy_unsafe$gc_ptr__builtins$u8){0}) // short alias for manual use diff --git a/spy/libspy/src/unsafe.c b/spy/libspy/src/unsafe.c index 5214db1ac..fc56c3957 100644 --- a/spy/libspy/src/unsafe.c +++ b/spy/libspy/src/unsafe.c @@ -10,6 +10,17 @@ spy_raw_alloc(size_t size) { return malloc(size); } +// Aligned variants for the interp (vm.ll.call) path. +void * +spy_raw_alloc_aligned(size_t size, size_t alignment) { + return spy_alloc_aligned_impl(size, alignment, spy_raw_alloc); +} + +void * +spy_nogc_alloc_aligned(size_t size, size_t alignment) { + return spy_alloc_aligned_impl(size, alignment, spy_nogc_alloc); +} + void _spy_memcpy(void *dst, void *src, size_t n) { memcpy(dst, src, n); diff --git a/spy/tests/compiler/unsafe/test_align_alloc.py b/spy/tests/compiler/unsafe/test_align_alloc.py new file mode 100644 index 000000000..0ba98dd69 --- /dev/null +++ b/spy/tests/compiler/unsafe/test_align_alloc.py @@ -0,0 +1,144 @@ +""" +Tests for the spy_alloc_aligned_impl C helper function. + +These tests exercise the *over-allocation* path: when the requested +alignment exceeds SPY_BASE_ALIGNMENT (16 on native-64, 8 on wasm), the +allocator can no longer rely on the base allocator's guarantee and must +over-allocate, round the pointer up, and discard the base pointer. +""" + +import pytest + +from spy.tests.support import CompilerTest + +# Alignment value guaranteed to exceed SPY_BASE_ALIGNMENT on all targets +# (16 on native-64, 8 on wasm). +OVER_ALIGNMENT = 32 + + +@pytest.fixture(params=["raw", "gc"]) +def memkind(request): + return request.param + + +class TestOverAllocAlignment(CompilerTest): + def test_ptr_address_is_aligned(self, memkind): + k = memkind + mod = self.compile( + """ + from unsafe import {k}_alloc as k_alloc, {k}_ptr as k_ptr, ptr_to_addr + + def alloc() -> k_ptr[i32, {N}]: + p = k_alloc[i32, {N}](4) + assert ptr_to_addr(p) % {N} == 0 + return p + """.format(k=k, N=OVER_ALIGNMENT) + ) + mod.alloc() + + def test_over_alloc_roundtrip(self, memkind): + k = memkind + mod = self.compile( + """ + from unsafe import {k}_alloc as k_alloc, {k}_ptr as k_ptr + + def foo() -> i32: + p: k_ptr[i32, {N}] = k_alloc[i32, {N}](3) + p[0] = 10 + p[1] = 20 + p[2] = 30 + return p[0] + p[1] + p[2] + """.format(k=k, N=OVER_ALIGNMENT) + ) + assert mod.foo() == 60 + + def test_over_alloc_struct(self, memkind): + k = memkind + mod = self.compile( + """ + from unsafe import {k}_alloc as k_alloc, {k}_ptr as k_ptr + + @struct + class Point: + x: i32 + y: i32 + + def foo() -> i32: + p: k_ptr[Point, {N}] = k_alloc[Point, {N}](2) + p[0].x = 1 + p[0].y = 2 + p[1].x = 3 + p[1].y = 4 + return p[0].x + 10*p[0].y + 100*p[1].x + 1000*p[1].y + """.format(k=k, N=OVER_ALIGNMENT) + ) + assert mod.foo() == 4321 + + def test_over_alloc_weakening(self): + mod = self.compile( + """ + from unsafe import gc_alloc, gc_ptr + + def foo() -> i32: + p32: gc_ptr[i32, {N}] = gc_alloc[i32, {N}](1) + # implicit weakening: gc_ptr[i32, {N}] -> gc_ptr[i32, 8] + p8: gc_ptr[i32, 8] = p32 + p8[0] = 99 + return p8[0] + """.format(N=OVER_ALIGNMENT) + ) + assert mod.foo() == 99 + + def test_over_alloc_multiple_are_aligned(self): + # Multiple independent over-aligned allocations should all be + # properly aligned (not just the first one) + mod = self.compile( + """ + from unsafe import gc_alloc, gc_ptr, ptr_to_addr + + def alloc() -> gc_ptr[i32, {N}]: + p = gc_alloc[i32, {N}](1) + assert ptr_to_addr(p) % {N} == 0 + return p + + def foo() -> i32: + a = alloc() + b = alloc() + c = alloc() + a[0] = 1 + b[0] = 2 + c[0] = 3 + return a[0] + b[0] + c[0] + """.format(N=OVER_ALIGNMENT) + ) + assert mod.foo() == 6 + # a few more independent allocations, for extra confidence + for _ in range(3): + mod.alloc() + + def test_over_alloc_f64(self): + mod = self.compile( + """ + from unsafe import gc_alloc, gc_ptr + + def foo() -> f64: + p: gc_ptr[f64, {N}] = gc_alloc[f64, {N}](2) + p[0] = 1.5 + p[1] = 2.5 + return p[0] + p[1] + """.format(N=OVER_ALIGNMENT) + ) + assert mod.foo() == 4.0 + + def test_over_alloc_address_is_aligned_f64(self): + mod = self.compile( + """ + from unsafe import gc_alloc, gc_ptr, ptr_to_addr + + def alloc() -> gc_ptr[f64, {N}]: + p = gc_alloc[f64, {N}](2) + assert ptr_to_addr(p) % {N} == 0 + return p + """.format(N=OVER_ALIGNMENT) + ) + mod.alloc() diff --git a/spy/tests/compiler/unsafe/test_alignof.py b/spy/tests/compiler/unsafe/test_alignof.py new file mode 100644 index 000000000..717086d2e --- /dev/null +++ b/spy/tests/compiler/unsafe/test_alignof.py @@ -0,0 +1,102 @@ +""" +`alignof` is usable from interp and app levels. +""" + +import pytest + +from spy.errors import SPyError +from spy.tests.support import CompilerTest, no_C +from spy.vm.b import B +from spy.vm.modules.unsafe.misc import alignof + + +def test_alignof_primitives(): + assert alignof(B.w_bool) == 1 + assert alignof(B.w_i8) == 1 + assert alignof(B.w_u8) == 1 + assert alignof(B.w_i32) == 4 + assert alignof(B.w_u32) == 4 + assert alignof(B.w_f32) == 4 + assert alignof(B.w_i64) == 8 + assert alignof(B.w_u64) == 8 + assert alignof(B.w_f64) == 8 + + +def test_alignof_not_implemented(): + with pytest.raises(SPyError, match="not implemented"): + alignof(B.w_dynamic) + + +@no_C +class TestAlign(CompilerTest): + def test_all_i32_fields(self): + mod = self.compile(""" + @struct + class Point: + x: i32 + y: i32 + """) + w_Point = mod.w_mod.getattr("Point") + assert alignof(w_Point) == 4 + + def test_mixed_field_sizes(self): + mod = self.compile(""" + @struct + class Mixed: + a: i8 + b: f64 + c: i32 + """) + w_Mixed = mod.w_mod.getattr("Mixed") + assert alignof(w_Mixed) == 8 + + def test_nested_struct(self): + mod = self.compile(""" + @struct + class Inner: + a: i8 + b: f64 + + @struct + class Outer: + x: i32 + inner: Inner + """) + w_Outer = mod.w_mod.getattr("Outer") + assert alignof(w_Outer) == 8 + + def test_empty_struct(self): + mod = self.compile(""" + @struct + class Empty: + pass + """) + w_Empty = mod.w_mod.getattr("Empty") + assert alignof(w_Empty) == 1 + + def test_primitive_blue(self): + mod = self.compile(""" + from unsafe import alignof + from __spy__ import COLOR + + def foo() -> i32: + N = alignof(i32) + assert COLOR(N) == "blue" + return N + """) + assert mod.foo() == 4 + + def test_struct(self): + mod = self.compile(""" + from unsafe import alignof + + @struct + class Mixed: + a: i8 + b: f64 + c: i32 + + def foo() -> i32: + return alignof(Mixed) + """) + assert mod.foo() == 8 diff --git a/spy/tests/compiler/unsafe/test_cast.py b/spy/tests/compiler/unsafe/test_cast.py new file mode 100644 index 000000000..0b5f1983e --- /dev/null +++ b/spy/tests/compiler/unsafe/test_cast.py @@ -0,0 +1,284 @@ +""" +Tests for cast[DstItemT](ptr) and align_cast[N](ptr). + +Note that ptr[T] has no app-level `.length` attribute (it's only accessible at +the interp/Python level, e.g. inside builtin funcs). So "did the length come +out right" is tested indirectly, via indexing: the last valid index must not +panic, and the first invalid index must panic. +""" + +import pytest + +from spy.errors import SPyError +from spy.tests.support import CompilerTest + + +class TestCastAlign(CompilerTest): + @pytest.fixture(params=["raw", "gc"]) + def memkind(self, request): + return request.param + + # ========================================================================= + # cast[DstItemT](ptr) + # ========================================================================= + + def test_cast_same_type(self, memkind): + k = memkind + mod = self.compile(f""" + from unsafe import {k}_alloc, {k}_ptr, cast + def test() -> i32: + p: {k}_ptr[i32] = {k}_alloc[i32](10) + q: {k}_ptr[i32] = cast[i32](p) + q[9] = 123 # last valid index for length 10 + return q[9] + """) + assert mod.test() == 123 + + def test_cast_same_type_out_of_bounds_panics(self, memkind): + k = memkind + mod = self.compile(f""" + from unsafe import {k}_alloc, {k}_ptr, cast + def test() -> i32: + p: {k}_ptr[i32] = {k}_alloc[i32](10) + q: {k}_ptr[i32] = cast[i32](p) + return q[10] # length is still 10, so this is out of bounds + """) + with pytest.raises(SPyError): + mod.test() + + def test_cast_same_size_types(self, memkind): + """cast between same-size types (i32 <-> f32) preserves length exactly.""" + k = memkind + mod = self.compile(f""" + from unsafe import {k}_alloc, {k}_ptr, cast + def test() -> i32: + p: {k}_ptr[i32] = {k}_alloc[i32](10) + q: {k}_ptr[f32] = cast[f32](p) + q[9] = 1.0 # last valid index for length 10 + return 1 + """) + assert mod.test() == 1 + + def test_cast_to_larger_type_truncates(self, memkind): + """10 i8 = 10 bytes; i32 is 4 bytes -> new length = 10 // 4 = 2.""" + k = memkind + mod = self.compile(f""" + from unsafe import {k}_alloc, {k}_ptr, cast + def ok() -> i32: + p: {k}_ptr[i8, 4] = {k}_alloc[i8, 4](10) + q: {k}_ptr[i32] = cast[i32](p) + q[1] = 5 + return q[1] + def bad() -> i32: + p: {k}_ptr[i8, 4] = {k}_alloc[i8, 4](10) + q: {k}_ptr[i32] = cast[i32](p) + return q[2] + """) + assert mod.ok() == 5 + with pytest.raises(SPyError): + mod.bad() + + def test_cast_to_smaller_type_truncates(self, memkind): + """10 i32 = 40 bytes; i8 is 1 byte -> new length = 40 // 1 = 40.""" + k = memkind + mod = self.compile(f""" + from unsafe import {k}_alloc, {k}_ptr, cast + def ok() -> i32: + p: {k}_ptr[i32] = {k}_alloc[i32](10) + q: {k}_ptr[i8, 4] = cast[i8](p) + q[39] = 7 + return q[39] + def bad() -> i32: + p: {k}_ptr[i32] = {k}_alloc[i32](10) + q: {k}_ptr[i8, 4] = cast[i8](p) + return q[40] + """) + assert mod.ok() == 7 + with pytest.raises(SPyError): + mod.bad() + + def test_cast_preserves_alignment(self, memkind): + k = memkind + mod = self.compile(f""" + from unsafe import {k}_alloc, {k}_ptr, cast + def test() -> i32: + p: {k}_ptr[i32, 16] = {k}_alloc[i32, 16](10) + q: {k}_ptr[f32, 16] = cast[f32](p) + return 1 + """) + assert mod.test() == 1 + + def test_cast_preserves_address(self, memkind): + k = memkind + mod = self.compile(f""" + from unsafe import {k}_alloc, {k}_ptr, cast, ptr_to_addr + def test() -> i32: + p: {k}_ptr[i32] = {k}_alloc[i32](10) + q: {k}_ptr[f32] = cast[f32](p) + assert ptr_to_addr(p) == ptr_to_addr(q) + return 1 + """) + assert mod.test() == 1 + + def test_cast_null_pointer(self, memkind): + k = memkind + mod = self.compile(f""" + from unsafe import {k}_ptr, cast, ptr_to_addr + def test() -> i32: + p: {k}_ptr[i32] = {k}_ptr[i32].NULL + q: {k}_ptr[f32] = cast[f32](p) + return ptr_to_addr(q) + """) + assert mod.test() == 0 + + def test_cast_zero_length(self, memkind): + k = memkind + mod = self.compile(f""" + from unsafe import {k}_ptr, cast + def test() -> i32: + p: {k}_ptr[i32] = {k}_ptr[i32].NULL + q: {k}_ptr[i32] = cast[i32](p) + return q[0] + """) + with pytest.raises(SPyError): + mod.test() + + def test_cast_chain(self, memkind): + """16 i8 -> 4 i32 -> 2 i64, chaining casts.""" + k = memkind + mod = self.compile(f""" + from unsafe import {k}_alloc, {k}_ptr, cast + def ok() -> i64: + p: {k}_ptr[i8, 8] = {k}_alloc[i8, 8](16) + q: {k}_ptr[i32, 8] = cast[i32](p) + r: {k}_ptr[i64, 8] = cast[i64](q) + r[1] = 9 + return r[1] + def bad() -> i64: + p: {k}_ptr[i8, 8] = {k}_alloc[i8, 8](16) + q: {k}_ptr[i32, 8] = cast[i32](p) + r: {k}_ptr[i64, 8] = cast[i64](q) + return r[2] + """) + assert mod.ok() == 9 + with pytest.raises(SPyError): + mod.bad() + + # ========================================================================= + # align_cast[N](ptr) + # ========================================================================= + + def test_align_cast_weaken(self, memkind): + k = memkind + mod = self.compile(f""" + from unsafe import {k}_alloc, {k}_ptr, align_cast + def test() -> i32: + p: {k}_ptr[i32, 16] = {k}_alloc[i32, 16](10) + q: {k}_ptr[i32, 4] = align_cast[4](p) + q[9] = 1 + return q[9] + """) + assert mod.test() == 1 + + def test_align_cast_same(self, memkind): + k = memkind + mod = self.compile(f""" + from unsafe import {k}_alloc, {k}_ptr, align_cast + def test() -> i32: + p: {k}_ptr[i32, 8] = {k}_alloc[i32, 8](10) + q: {k}_ptr[i32, 8] = align_cast[8](p) + q[9] = 1 + return q[9] + """) + assert mod.test() == 1 + + def test_align_cast_strengthen_when_actually_aligned(self, memkind): + k = memkind + mod = self.compile(f""" + from unsafe import {k}_alloc, {k}_ptr, align_cast + def test() -> i32: + strong: {k}_ptr[i8, 4096] = {k}_alloc[i8, 4096](1) + weak: {k}_ptr[i8, 1] = strong + back: {k}_ptr[i8, 4096] = align_cast[4096](weak) + return 1 + """) + assert mod.test() == 1 + + def test_align_cast_strengthen_invalid_panics(self, memkind): + """ + A 1-byte allocation has no reason to land on a 4096-byte boundary; + claiming that alignment should panic (this isn't a mathematical certainty + but is as close as we can get without exposing pointer arithmetic). + """ + k = memkind + mod = self.compile(f""" + from unsafe import {k}_alloc, {k}_ptr, align_cast + def test() -> i32: + p: {k}_ptr[i8, 1] = {k}_alloc[i8, 1](1) + q: {k}_ptr[i8, 4096] = align_cast[4096](p) + return q[0] + """) + with pytest.raises(SPyError): + mod.test() + + def test_align_cast_preserves_address(self, memkind): + k = memkind + mod = self.compile(f""" + from unsafe import {k}_alloc, {k}_ptr, align_cast, ptr_to_addr + def test() -> i32: + p: {k}_ptr[i32, 4] = {k}_alloc[i32, 4](10) + q: {k}_ptr[i32, 8] = align_cast[8](p) + assert ptr_to_addr(p) == ptr_to_addr(q) + return 1 + """) + assert mod.test() == 1 + + def test_align_cast_preserves_type(self, memkind): + k = memkind + mod = self.compile(f""" + from unsafe import {k}_alloc, {k}_ptr, align_cast + def test() -> i32: + p: {k}_ptr[i32, 4] = {k}_alloc[i32, 4](10) + q: {k}_ptr[i32, 8] = align_cast[8](p) + q[0] = 42 + return q[0] + """) + assert mod.test() == 42 + + def test_align_cast_null_pointer(self, memkind): + """NULL (addr 0) satisfies any alignment, so this never panics.""" + k = memkind + mod = self.compile(f""" + from unsafe import {k}_ptr, align_cast, ptr_to_addr + def test() -> i32: + p: {k}_ptr[i32] = {k}_ptr[i32].NULL + q: {k}_ptr[i32, 16] = align_cast[16](p) + return ptr_to_addr(q) + """) + assert mod.test() == 0 + + def test_compose_cast_and_align_cast(self, memkind): + k = memkind + mod = self.compile(f""" + from unsafe import {k}_alloc, {k}_ptr, cast, align_cast + def test() -> i32: + p: {k}_ptr[i32, 4] = {k}_alloc[i32, 4](10) + q: {k}_ptr[f32, 4] = cast[f32](p) + r: {k}_ptr[f32, 8] = align_cast[8](q) + r[9] = 1.0 + return 1 + """) + assert mod.test() == 1 + + def test_align_cast_chain(self, memkind): + k = memkind + mod = self.compile(f""" + from unsafe import {k}_alloc, {k}_ptr, align_cast + def test() -> i32: + p: {k}_ptr[i32, 4] = {k}_alloc[i32, 4](10) + q: {k}_ptr[i32, 8] = align_cast[8](p) + r: {k}_ptr[i32, 16] = align_cast[16](q) + r[9] = 1 + return r[9] + """) + assert mod.test() == 1 diff --git a/spy/tests/compiler/unsafe/test_ptr.py b/spy/tests/compiler/unsafe/test_ptr.py index 529b618cc..314a59f68 100644 --- a/spy/tests/compiler/unsafe/test_ptr.py +++ b/spy/tests/compiler/unsafe/test_ptr.py @@ -5,6 +5,7 @@ from spy.tests.wasm_wrapper import WasmPtr from spy.vm.b import B from spy.vm.modules.unsafe import UNSAFE +from spy.vm.modules.unsafe.misc import alignof from spy.vm.modules.unsafe.ptr import W_Ptr @@ -720,8 +721,7 @@ def get_byte(s: str, i: i32) -> u8: assert mod.get_byte("hello", 4) == ord("o") def test_ptr_index_all_dtypes(self): - mod = self.compile( - """ + mod = self.compile(""" from unsafe import gc_alloc, gc_ptr def rt[T](v: T) -> T: @@ -737,8 +737,7 @@ def rt[T](v: T) -> T: rt_u64 = rt[u64] rt_f32 = rt[f32] rt_f64 = rt[f64] - """ - ) + """) assert mod.rt_i8(-(2**7)) == -(2**7) assert mod.rt_u8(2**8 - 1) == 2**8 - 1 assert mod.rt_i32(-(2**31)) == -(2**31) @@ -835,3 +834,86 @@ def rt_struct_byval_f32(v: f32) -> f32: assert mod.rt_f32(7) == 7.0 assert mod.rt_f64(1.5) == 1.5 assert mod.rt_struct_byval_f32(7) == 7.0 + + @only_interp + def test_default_alignment_matches_alignof(self): + w_default = self.vm.fast_call(UNSAFE.w_gc_ptr, [B.w_f64]) + N = alignof(B.w_f64) + w_explicit = self.vm.fast_call(UNSAFE.w_gc_ptr, [B.w_f64, self.vm.wrap(N)]) + assert w_default is w_explicit + assert repr(w_default) == "" + + @only_interp + def test_explicit_alignment_in_fqn(self): + w_ptrtype = self.vm.fast_call(UNSAFE.w_gc_ptr, [B.w_i32, self.vm.wrap(8)]) + assert repr(w_ptrtype) == "" + + @only_interp + def test_different_alignments_are_different_types(self): + w_8 = self.vm.fast_call(UNSAFE.w_gc_ptr, [B.w_i32, self.vm.wrap(8)]) + w_16 = self.vm.fast_call(UNSAFE.w_gc_ptr, [B.w_i32, self.vm.wrap(16)]) + assert w_8 is not w_16 + + @only_interp + def test_too_many_arguments(self): + with pytest.raises(SPyError, match="accepts 1 or 2 arguments"): + self.vm.fast_call( + UNSAFE.w_gc_ptr, + [B.w_i32, self.vm.wrap(8), self.vm.wrap(16)], + ) + + def test_explicit_alignment_roundtrip(self): + mod = self.compile(""" + from unsafe import gc_alloc, gc_ptr + + def foo() -> i32: + p: gc_ptr[i32, 8] = gc_alloc[i32, 8](1) + p[0] = 42 + return p[0] + """) + assert mod.foo() == 42 + + def test_default_equals_explicit_alignof(self): + mod = self.compile(""" + from unsafe import gc_alloc, gc_ptr + + def foo() -> i32: + p: gc_ptr[i32] = gc_alloc[i32, 4](1) + p[0] = 7 + return p[0] + """) + assert mod.foo() == 7 + + def test_weakening_is_implicit(self): + mod = self.compile(""" + from unsafe import gc_alloc, gc_ptr + + def foo() -> i32: + p16: gc_ptr[i32, 16] = gc_alloc[i32, 16](1) + # implicit weakening: gc_ptr[i32, 16] -> gc_ptr[i32, 8] + p8: gc_ptr[i32, 8] = p16 + p8[0] = 123 + return p8[0] + """) + assert mod.foo() == 123 + + def test_strengthening_is_a_type_error(self): + src = """ + from unsafe import gc_alloc, gc_ptr + + def foo() -> None: + p8: gc_ptr[i32, 8] = gc_alloc[i32, 8](1) + p16: gc_ptr[i32, 16] = p8 + """ + errors = expect_errors( + "mismatched types", + ( + "expected `unsafe::gc_ptr[i32, 16]`, got `unsafe::gc_ptr[i32, 8]`", + "p8", + ), + ( + "expected `unsafe::gc_ptr[i32, 16]` because of type declaration", + "gc_ptr[i32, 16]", + ), + ) + self.compile_raises(src, "foo", errors) diff --git a/spy/tests/compiler/unsafe/test_under_align.py b/spy/tests/compiler/unsafe/test_under_align.py new file mode 100644 index 000000000..818741c4f --- /dev/null +++ b/spy/tests/compiler/unsafe/test_under_align.py @@ -0,0 +1,73 @@ +""" +Tests for load/store with __builtin_memcpy when alignment < alignof(T). +""" + +import pytest + +from spy.tests.support import CompilerTest + + +class TestUnderAligned(CompilerTest): + @pytest.fixture(params=["raw", "gc"]) + def memkind(self, request): + return request.param + + def test_under_aligned_roundtrip(self, memkind): + k = memkind + mod = self.compile(f""" + from unsafe import {k}_alloc as k_alloc, {k}_ptr as k_ptr + def foo[T]() -> T: + p: k_ptr[T, 1] = k_alloc[T, 1](3) + p[0] = 10; p[1] = 20; p[2] = 30 + return p[0] + p[1] + p[2] + + foo_i32 = foo[i32] + foo_f64 = foo[f64] + """) + assert mod.foo_i32() == 60 + assert mod.foo_f64() == 60.0 + + def test_weaken_to_under_aligned(self): + mod = self.compile(""" + from unsafe import gc_alloc, gc_ptr + def foo[T]() -> T: + p_aligned: gc_ptr[T] = gc_alloc[T](2) + p1: gc_ptr[T, 1] = p_aligned + p1[0] = 99; p1[1] = -7 + return p1[0] - p1[1] + + foo_i32 = foo[i32] + foo_f64 = foo[f64] + """) + assert mod.foo_i32() == 106 + assert mod.foo_f64() == 106.0 + + def test_struct_field_roundtrip(self): + mod = self.compile(""" + from unsafe import gc_alloc, gc_ptr + @struct + class Point: + x: i32 + y: i32 + def foo() -> i32: + p: gc_ptr[Point, 1] = gc_alloc[Point, 1](2) + p[0].x = 1; p[0].y = 2 + p[1].x = 3; p[1].y = 4 + return p[0].x + 10*p[0].y + 100*p[1].x + 1000*p[1].y + """) + assert mod.foo() == 4321 + + def test_struct_field_weakening(self): + mod = self.compile(""" + from unsafe import gc_alloc, gc_ptr + @struct + class Point: + x: i32 + y: i32 + def foo() -> i32: + p32: gc_ptr[Point, 32] = gc_alloc[Point, 32](1) + p1: gc_ptr[Point, 1] = p32 + p1[0].x = 100; p1[0].y = 200 + return p1[0].x + p1[0].y + """) + assert mod.foo() == 300 diff --git a/spy/vm/modules/unsafe/__init__.py b/spy/vm/modules/unsafe/__init__.py index d05b7616e..c705ddb1f 100644 --- a/spy/vm/modules/unsafe/__init__.py +++ b/spy/vm/modules/unsafe/__init__.py @@ -16,7 +16,9 @@ UNSAFE = ModuleRegistry("unsafe") from . import ( + cast, # noqa: F401 -- side effects div, # noqa: F401 -- side effects mem, # noqa: F401 -- side effects + misc, # noqa: F401 -- side effects ptr, # noqa: F401 -- side effects ) diff --git a/spy/vm/modules/unsafe/cast.py b/spy/vm/modules/unsafe/cast.py new file mode 100644 index 000000000..8551ce318 --- /dev/null +++ b/spy/vm/modules/unsafe/cast.py @@ -0,0 +1,135 @@ +""" +Pointer type casting and alignment casting for the unsafe module. + +Provides: +- cast[DstItemT](ptr): changes item type, preserves address and alignment +- align_cast[N](ptr): changes alignment tag, preserves address and item type +""" + +from typing import TYPE_CHECKING, Annotated + +from spy.errors import SPyError +from spy.vm.irtag import IRTag +from spy.vm.opspec import W_MetaArg, W_OpSpec +from spy.vm.primitive import W_I32, W_Dynamic +from spy.vm.w import W_Type + +from . import UNSAFE +from .misc import sizeof +from .ptr import W_Ptr, W_PtrType, w_gc_ptr, w_raw_ptr + +if TYPE_CHECKING: + from spy.vm.vm import SPyVM + + +def _check_ptr_static(vm: "SPyVM", wam_ptr: W_MetaArg, opname: str) -> W_PtrType: + """ + Validate that wam_ptr is statically typed as ptr[T] (any memkind, any T). + Mirrors unsafe/mem.py::_check_ptr. + """ + w_T = wam_ptr.w_static_T + if isinstance(w_T, W_PtrType): + return w_T + t = w_T.fqn.human_name(vm) + err = SPyError("W_TypeError", "mismatched types") + err.add("error", f"{opname}: expected ptr[T], got `{t}`", loc=wam_ptr.loc) + raise err + + +def _same_memkind_ptrtype( + vm: "SPyVM", w_srcT: W_PtrType, w_itemT: W_Type, alignment: int +) -> W_PtrType: + """ + raw_ptr[w_itemT, alignment] or gc_ptr[w_itemT, alignment], matching the + memkind of w_srcT. + """ + w_ctor = w_raw_ptr if w_srcT.memkind == "raw" else w_gc_ptr + w_dstT = vm.fast_call(w_ctor, [w_itemT, vm.wrap(alignment)]) + assert isinstance(w_dstT, W_PtrType) + return w_dstT + + +# ============================================================================= +# cast[DstItemT](ptr) +# ============================================================================= + + +@UNSAFE.builtin_func(color="blue", kind="generic") +def w_cast(vm: "SPyVM", w_DstItemT: W_Type) -> W_Dynamic: + """ + cast[DstItemT] -> a metafunc, blue-cached per DstItemT. + + Calling the metafunc with a ptr produces: + raw_ptr[DstItemT, N] or gc_ptr[DstItemT, N] + where N is the SOURCE ptr's alignment (preserved) and the memkind also + matches the source. The length is recomputed from the byte size, with + truncation. This operation is always safe and has zero runtime overhead. + """ + ns = UNSAFE.w_cast.compute_inner_ns([w_DstItemT]) + + @vm.register_builtin_func(ns, "impl", color="blue", kind="metafunc") + def w_cast_dispatch(vm: "SPyVM", wam_ptr: W_MetaArg) -> W_OpSpec: + w_srcT = _check_ptr_static(vm, wam_ptr, "cast") + w_dstT = _same_memkind_ptrtype(vm, w_srcT, w_DstItemT, w_srcT.alignment) + + src_size = sizeof(w_srcT.w_itemT) + dst_size = sizeof(w_DstItemT) + + SRC = Annotated[W_Ptr, w_srcT] + DST = Annotated[W_Ptr, w_dstT] + irtag = IRTag("unsafe.cast") + + @vm.register_builtin_func(w_srcT.fqn, "cast", [w_DstItemT.fqn], irtag=irtag) + def w_cast_impl(vm: "SPyVM", w_ptr: SRC) -> DST: + new_length = (w_ptr.length * src_size) // dst_size + return W_Ptr(w_dstT, w_ptr.addr, new_length) # type: ignore + + return W_OpSpec(w_cast_impl, [wam_ptr]) + + return w_cast_dispatch + + +# ============================================================================= +# align_cast[N](ptr) +# ============================================================================= + + +@UNSAFE.builtin_func(color="blue", kind="generic") +def w_align_cast(vm: "SPyVM", w_N: W_I32) -> W_Dynamic: + """ + align_cast[N] -> a metafunc, blue-cached per N. + + Calling the metafunc with a ptr produces raw_ptr[T, N]/gc_ptr[T, N] + (same memkind and item type as the source, new alignment N): + + - Weakening (N <= old_alignment): free conversion, always safe. Note + this is also handled implicitly by W_CONVERT_TO for plain assignment; + - Strengthening (N > old_alignment): asserts addr % N == 0 in DEBUG + mode (both interp and C); trusts the claim in RELEASE mode (C only; + the interpreter always checks). + """ + N = vm.unwrap_i32(w_N) + ns = UNSAFE.w_align_cast.fqn.with_qualifiers([str(N)]) + + @vm.register_builtin_func(ns, "impl", color="blue", kind="metafunc") + def w_align_cast_dispatch(vm: "SPyVM", wam_ptr: W_MetaArg) -> W_OpSpec: + w_srcT = _check_ptr_static(vm, wam_ptr, "align_cast") + old_alignment = w_srcT.alignment + w_dstT = _same_memkind_ptrtype(vm, w_srcT, w_srcT.w_itemT, N) + + SRC = Annotated[W_Ptr, w_srcT] + DST = Annotated[W_Ptr, w_dstT] + irtag = IRTag("unsafe.align_cast", new_alignment=N, old_alignment=old_alignment) + + @vm.register_builtin_func(w_srcT.fqn, "align_cast", [str(N)], irtag=irtag) + def w_align_cast_impl(vm: "SPyVM", w_ptr: SRC) -> DST: + if N > old_alignment and w_ptr.addr % N != 0: + raise SPyError( + "W_PanicError", + f"align_cast: address 0x{w_ptr.addr:x} not aligned to {N}", + ) + return W_Ptr(w_dstT, w_ptr.addr, w_ptr.length) # type: ignore + + return W_OpSpec(w_align_cast_impl, [wam_ptr]) + + return w_align_cast_dispatch diff --git a/spy/vm/modules/unsafe/mem.py b/spy/vm/modules/unsafe/mem.py index e6fda34f5..172945501 100644 --- a/spy/vm/modules/unsafe/mem.py +++ b/spy/vm/modules/unsafe/mem.py @@ -10,18 +10,44 @@ from spy.vm.w import W_Object, W_Type from . import UNSAFE -from .misc import sizeof +from .misc import parse_optional_alignment, sizeof from .ptr import W_Ptr, W_PtrType, w_gc_ptr, w_raw_ptr if TYPE_CHECKING: from spy.vm.vm import SPyVM +# The base alignment that the interp-level allocators (spy_raw_alloc, +# spy_nogc_alloc, which are just malloc) already guarantee. This must +# match the C-side SPY_BASE_ALIGNMENT in spy/libspy/include/spy/unsafe.h. +# On wasm32/wasm64, malloc returns 8-byte-aligned pointers; on native +# 16-byte alignment. +SPY_BASE_ALIGNMENT_INTERP = 8 + + @UNSAFE.builtin_func(color="blue", kind="generic") -def w_raw_alloc(vm: "SPyVM", w_T: W_Type) -> W_Dynamic: - w_ptrtype = vm.fast_call(w_raw_ptr, [w_T]) # unsafe::raw_ptr[i32] +def w_raw_alloc(vm: "SPyVM", w_T: W_Type, *args_w: W_Dynamic) -> W_Dynamic: + if len(args_w) == 0: + # raw_alloc[T] means "default alignment", same as raw_ptr[T]. Go + # through the 0-arg raw_ptr[T] call rather than pre-resolving + # alignof(T) here: this lands on the SAME blue-cache entry that a + # field declaration like `next: raw_ptr[Node]` produces, so we get + # the identical W_PtrType object back. + # + # If we instead resolved alignof(T) ourselves and called + # raw_ptr[T, alignof(T)], the blue-cache key would differ whenever + # T is a not-yet-defined (e.g. self-referential) struct: alignof(T) + # is 1 during the struct body but its real value after definition. + # That would create a second W_PtrType with the same FQN and trip + # make_fqn_const's uniqueness assertion. + w_ptrtype = vm.fast_call(w_raw_ptr, [w_T]) + else: + alignment = parse_optional_alignment(vm, w_T, args_w, "raw_alloc") + w_N = vm.wrap(alignment) + w_ptrtype = vm.fast_call(w_raw_ptr, [w_T, w_N]) # unsafe::raw_ptr[...] assert isinstance(w_ptrtype, W_PtrType) ITEMSIZE = sizeof(w_T) + ALIGNMENT = w_ptrtype.alignment # unsafe::raw_ptr[i32]::alloc # @@ -31,17 +57,29 @@ def w_raw_alloc(vm: "SPyVM", w_T: W_Type) -> W_Dynamic: def w_fn(vm: "SPyVM", w_n: W_I32) -> Annotated[W_Ptr, w_ptrtype]: n = vm.unwrap_i32(w_n) size = ITEMSIZE * n - addr = vm.ll.call("spy_raw_alloc", size) + if ALIGNMENT <= SPY_BASE_ALIGNMENT_INTERP: + # the allocator already guarantees this alignment + addr = vm.ll.call("spy_raw_alloc", size) + else: + # over-allocate and round up, mirroring spy_alloc_aligned_impl + addr = vm.ll.call("spy_raw_alloc_aligned", size, ALIGNMENT) return W_Ptr(w_ptrtype, addr, n) # type: ignore return w_fn @UNSAFE.builtin_func(color="blue", kind="generic") -def w_gc_alloc(vm: "SPyVM", w_T: W_Type) -> W_Dynamic: - w_ptrtype = vm.fast_call(w_gc_ptr, [w_T]) # unsafe::gc_ptr[i32] +def w_gc_alloc(vm: "SPyVM", w_T: W_Type, *args_w: W_Dynamic) -> W_Dynamic: + if len(args_w) == 0: + # see the comment in w_raw_alloc above + w_ptrtype = vm.fast_call(w_gc_ptr, [w_T]) + else: + alignment = parse_optional_alignment(vm, w_T, args_w, "gc_alloc") + w_N = vm.wrap(alignment) + w_ptrtype = vm.fast_call(w_gc_ptr, [w_T, w_N]) # unsafe::gc_ptr[...] assert isinstance(w_ptrtype, W_PtrType) ITEMSIZE = sizeof(w_T) + ALIGNMENT = w_ptrtype.alignment # unsafe::gc_ptr[i32]::alloc # @@ -51,7 +89,12 @@ def w_gc_alloc(vm: "SPyVM", w_T: W_Type) -> W_Dynamic: def w_fn(vm: "SPyVM", w_n: W_I32) -> Annotated[W_Ptr, w_ptrtype]: n = vm.unwrap_i32(w_n) size = ITEMSIZE * n - addr = vm.ll.call("spy_nogc_alloc", size) + if ALIGNMENT <= SPY_BASE_ALIGNMENT_INTERP: + # the allocator already guarantees this alignment + addr = vm.ll.call("spy_nogc_alloc", size) + else: + # over-allocate and round up, mirroring spy_alloc_aligned_impl + addr = vm.ll.call("spy_nogc_alloc_aligned", size, ALIGNMENT) return W_Ptr(w_ptrtype, addr, n) # type: ignore return w_fn diff --git a/spy/vm/modules/unsafe/misc.py b/spy/vm/modules/unsafe/misc.py index 387369104..29b148fd1 100644 --- a/spy/vm/modules/unsafe/misc.py +++ b/spy/vm/modules/unsafe/misc.py @@ -1,6 +1,14 @@ -from spy.errors import WIP +from typing import TYPE_CHECKING + +from spy.errors import WIP, SPyError from spy.vm.b import B from spy.vm.object import W_Type +from spy.vm.primitive import W_I32 + +from . import UNSAFE + +if TYPE_CHECKING: + from spy.vm.vm import SPyVM def sizeof(w_T: W_Type) -> int: @@ -73,3 +81,74 @@ def contains_gc_ptr(w_T: W_Type) -> bool: return True raise NotImplementedError(f"{w_T=}") + + +def alignof(w_T: W_Type) -> int: + """ + The natural alignment of a type, in bytes. + """ + from spy.vm.modules.posix import POSIX + from spy.vm.modules.unsafe.ptr import W_PtrType, W_RefType + from spy.vm.struct import W_StructType + + # for every scalar type SPy has today, natural alignment == size + if w_T in (B.w_bool, B.w_i8, B.w_u8): + return 1 + elif w_T in (B.w_i32, B.w_u32, B.w_f32): + return 4 + elif w_T in (B.w_i64, B.w_u64, B.w_f64): + return 8 + elif isinstance(w_T, (W_PtrType, W_RefType)) or w_T is B.w_str: + # pointers are 4 bytes on wasm32; see the comment in sizeof() + return 4 + elif w_T is POSIX.w__FILE: + return 4 + elif isinstance(w_T, W_StructType): + if not w_T.is_defined(): + # not-yet-defined struct (e.g. a struct that (transitively) + # points to itself, or one of the special bootstrapping + # struct types like _str::StrObject that this function may be + # asked about before its fields are populated -- see the + # analogous comment in W_MemLocType.from_itemtype). We can't + # look at fields that don't exist yet, so fall back to 1 + # rather than crashing; + return 1 + # the usual "max of the fields' alignments" rule. A struct with no + # fields has nothing to take the max over, so fall back to 1 + # (matching a struct of size 0) rather than raising. + aligns = [alignof(w_field.w_T) for w_field in w_T.iterfields_w()] + return max(aligns, default=1) + else: + raise WIP(f"alignof({w_T}) not implemented") + + +@UNSAFE.builtin_func(color="blue") +def w_alignof(vm: "SPyVM", w_T: W_Type) -> W_I32: + """ + The SPy-visible `alignof(T)` blue builtin. + """ + return vm.wrap(alignof(w_T)) + + +def parse_optional_alignment( + vm: "SPyVM", w_T: W_Type, args_w: tuple, funcname: str +) -> int: + """ + Shared arg-parsing for the optional, defaulted alignment type param on + {raw,gc}_ptr[T, N=alignof(T)] / {raw,gc}_alloc[T, N=alignof(T)]. + `args_w` is whatever extra positional blue args were + passed after `T`: zero (use the default) or one (an i32 `N`). + """ + if len(args_w) == 0: + return alignof(w_T) + elif len(args_w) == 1: + w_N = args_w[0] + if not isinstance(w_N, W_I32): + t = vm.dynamic_type(w_N).fqn.human_name(vm) + raise SPyError( + "W_TypeError", f"{funcname}: alignment must be i32, got `{t}`" + ) + return int(vm.unwrap_i32(w_N)) + else: + n = len(args_w) + 1 + raise SPyError("W_TypeError", f"{funcname} accepts 1 or 2 arguments, got {n}") diff --git a/spy/vm/modules/unsafe/ptr.py b/spy/vm/modules/unsafe/ptr.py index 3ec6c8759..818d7dbd3 100644 --- a/spy/vm/modules/unsafe/ptr.py +++ b/spy/vm/modules/unsafe/ptr.py @@ -30,11 +30,9 @@ from spy.errors import SPyError from spy.fqn import FQN -from spy.location import Loc from spy.vm.b import B from spy.vm.builtin import builtin_method, builtin_property from spy.vm.bytes import W_Bytes -from spy.vm.function import W_ASTFunc from spy.vm.irtag import IRTag from spy.vm.member import Member from spy.vm.modules.types import W_Loc @@ -44,7 +42,7 @@ from spy.vm.w import W_Func, W_Object, W_Str, W_Type from . import UNSAFE -from .misc import sizeof +from .misc import alignof, parse_optional_alignment, sizeof if TYPE_CHECKING: from spy.vm.vm import SPyVM @@ -54,25 +52,52 @@ @UNSAFE.builtin_func(color="blue", kind="generic") -def w_raw_ptr(vm: "SPyVM", w_T: W_Type) -> W_Dynamic: - """ - The raw_ptr[T] generic type - """ - fqn = FQN("unsafe").join("raw_ptr", [w_T.fqn]) # unsafe::raw_ptr[i32] - w_ptrtype = W_PtrType.from_itemtype(fqn, "raw", w_T) +def w_raw_ptr(vm: "SPyVM", w_T: W_Type, *args_w: W_Dynamic) -> W_Dynamic: + """ + The raw_ptr[T] / raw_ptr[T, N] generic type + """ + if len(args_w) == 0: + # raw_ptr[T] is just the common-case spelling of + # raw_ptr[T, alignof(T)]. Type identity across calls comes purely + # from the blue-call cache, which keys on (func, args_w). So we + # recurse through the 2-arg call, landing on the same cache entry + # and therefore the same W_PtrType object. + w_N = vm.wrap(alignof(w_T)) + return vm.fast_call(w_raw_ptr, [w_T, w_N]) + alignment = parse_optional_alignment(vm, w_T, args_w, "raw_ptr") + fqn = _ptr_fqn("raw_ptr", w_T, alignment) + w_ptrtype = W_PtrType.from_itemtype(fqn, "raw", w_T, alignment) return w_ptrtype @UNSAFE.builtin_func(color="blue", kind="generic") -def w_gc_ptr(vm: "SPyVM", w_T: W_Type) -> W_Dynamic: +def w_gc_ptr(vm: "SPyVM", w_T: W_Type, *args_w: W_Dynamic) -> W_Dynamic: """ - The gc_ptr[T] generic type + The gc_ptr[T] / gc_ptr[T, N] generic type """ - fqn = FQN("unsafe").join("gc_ptr", [w_T.fqn]) # unsafe::gc_ptr[i32] - w_ptrtype = W_PtrType.from_itemtype(fqn, "gc", w_T) + if len(args_w) == 0: + # see the comment in w_raw_ptr above + w_N = vm.wrap(alignof(w_T)) + return vm.fast_call(w_gc_ptr, [w_T, w_N]) + alignment = parse_optional_alignment(vm, w_T, args_w, "gc_ptr") + fqn = _ptr_fqn("gc_ptr", w_T, alignment) + w_ptrtype = W_PtrType.from_itemtype(fqn, "gc", w_T, alignment) return w_ptrtype +def _ptr_fqn(funcname: str, w_T: W_Type, alignment: int) -> FQN: + """ + unsafe::{raw,gc}_ptr[T] when `alignment` is T's natural alignment + (the default) else unsafe::{raw,gc}_ptr[T, N], so that an explicit, + non-default alignment is part of the type's identity + (gc_ptr[T,N] and gc_ptr[T,M] for N != M are distinct types). + """ + qualifiers: list = [w_T.fqn] + if alignment != alignof(w_T): + qualifiers.append(str(alignment)) + return FQN("unsafe").join(funcname, qualifiers) + + @UNSAFE.builtin_func(color="blue", kind="generic") def w_raw_ref(vm: "SPyVM", w_T: W_Type) -> W_Dynamic: """ @@ -167,10 +192,17 @@ class W_MemLocType(W_Type): memkind: MEMKIND w_itemT: Annotated[W_Type, Member("itemtype")] + alignment: int is_ready: bool @classmethod - def from_itemtype(cls, fqn: FQN, memkind: MEMKIND, w_itemT: W_Type) -> Self: + def from_itemtype( + cls, + fqn: FQN, + memkind: MEMKIND, + w_itemT: W_Type, + alignment: Optional[int] = None, + ) -> Self: if cls is W_PtrType: w_T = cls.from_pyclass(fqn, W_Ptr) elif cls is W_RefType: @@ -179,6 +211,11 @@ def from_itemtype(cls, fqn: FQN, memkind: MEMKIND, w_itemT: W_Type) -> Self: assert False w_T.memkind = memkind w_T.w_itemT = w_itemT + if alignment is None: + from .misc import alignof + + alignment = alignof(w_itemT) + w_T.alignment = alignment w_T.is_ready = False if isinstance(w_itemT, W_StructType): if w_itemT.is_defined(): @@ -472,6 +509,25 @@ def w_ptr_to_bool(vm: "SPyVM", w_ptr: PTR) -> W_Bool: return W_OpSpec(w_ptr_to_bool) + elif ( + isinstance(w_T, W_PtrType) + and w_T.memkind == w_ptrtype.memkind + and w_T.w_itemT is w_ptrtype.w_itemT + and w_T.alignment <= w_ptrtype.alignment + ): + # weakening conversion: gc_ptr[T,N] -> gc_ptr[T,M] is free whenever + # M <= N. The strengthening direction (M > N) is NOT handled here + # (needs align_cast). + TARGET = Annotated[W_Ptr, w_T] + funcname = f"weaken_align_to_{w_T.alignment}" + irtag = IRTag("ptr.weaken_align") + + @vm.register_builtin_func(w_ptrtype.fqn, funcname, irtag=irtag) + def w_ptr_weaken_align(vm: "SPyVM", w_ptr: PTR) -> TARGET: + return W_Ptr(w_T, w_ptr.addr, w_ptr.length) # type: ignore + + return W_OpSpec(w_ptr_weaken_align) + else: return W_OpSpec.NULL @@ -574,6 +630,26 @@ def w_ptr_setfield_T( return w_ptr_setfield_T +@UNSAFE.builtin_func(color="blue", kind="metafunc") +def w_ptr_to_addr(vm: "SPyVM", wam_p: W_MetaArg) -> W_OpSpec: + """ + Return the address that a raw_ptr/gc_ptr points to, as an i32. + + NOTE: like W_MemLoc.addr, this only works correctly for wasm32-like + targets where addresses fit in 32 bits. It's mostly meant for tests, + debugging, and assertions (e.g. checking alignment) -- not as a + general "pointer as integer" escape hatch. + """ + w_ptrtype = W_Ptr._get_memlocT(wam_p) + PTR = Annotated[W_Ptr, w_ptrtype] + + @vm.register_builtin_func(w_ptrtype.fqn, "to_addr") + def w_ptr_to_addr_impl(vm: "SPyVM", w_ptr: PTR) -> W_I32: + return vm.wrap(w_ptr.addr) + + return W_OpSpec(w_ptr_to_addr_impl, [wam_p]) + + @UNSAFE.builtin_func(color="blue", kind="metafunc") def w__str_to_StrObject(vm: "SPyVM", wam_s: W_MetaArg) -> W_OpSpec: """