crystal
crystal copied to clipboard
Value corruption with Slice on reused variable
I'm using Crystal 1.16.3 [3f369d2c7] (2025-05-12), LLVM: 18.1.8, Default target: x86_64-unknown-linux-gnu
x = 0x12345678u32 # mind the u32
x, y = 0x1234u16, 0x1234u16 # assignment to both same and fresh variable
ptr1 = pointerof(x).as(UInt8*)
p Slice.new(ptr1, 2) # Bytes[184, 0] # bad
ptr2 = pointerof(y).as(UInt8*)
p Slice.new(ptr2, 2) # Bytes[52, 18] # good
The type of pointerof(x) is Pointer(UInt16 | UInt32). But typeof(x) is UInt16.
IMO it's very unexpected that typeof(pointerof(x)) != Pointer(typeof(x)).
The pointer does not directly reference the value because the union type starts with a type tag.
You can observe this by inspecting the byte representation of x:
Slice.new(pointerof(x), 1).to_unsafe_bytes # => Bytes[184, 0, 0, 0, 0, 0, 0, 0, 52, 18, 52, 18, 0, 0, 0, 0]
184 is the type id of UInt16:
0_u16.crystal_type_id # => 184
I'm not quite sure why pointerof(x) is a union pointer. The compiler knows that due to flow typing, the variable is of type UInt16 at this point. It cannot be UInt32. That was the type from the first assignment to x. It's later overwritten by the second assignment.
They're essentially two separate variables which just happen to share the name. Their lifetimes are mutually exclusive. And that allos them to use the same memory location. But I'd consider this an implementation detail. It's a compiler optimization to reduce memory usage. That should not leak outside.
This weird union pointer leads to other surprising effects. I can successfully set the pointer's value to a UInt32 value, while the variable is still typed as UInt16.
x = 0_u32
x = 0_u16
pointerof(x).value = UInt32::MAX
x # => 65535 (UIn16::MAX)
pointerof(x).value # => 4294967295 (UInt32::MAX)
This leads to the surprising result that x != pointerof(x).value.
The problem (to lay it out explicitly):
x = 1_u32
x = 1_u16
typeof(x) # => UInt16
typeof(pointerof(x)) # => Pointer(UInt16 | UInt32)
As discussed internally, this is expected behavior: the compiler internally makes x to be a union, even though it transparently handles it as a variable that changes its type with each assignment. pointerof exposes the true nature of the variable and, as shown, can lead to unexpected behavior. This requires proper documentation.
The real fix is to use proper shadowing: to make each different assignment to x create a fresh variable, without sharing their space.