odin-lang/Odin Issue #4882: Unexpected behavior for #shared_nil on union of pointer types
2025-02-26 08:58:17 nb-ohad
Context
A union of pointers of types, tagged with `#shared_nil`, treats a pointer to an empty struct type (pointer to `struct {}`) as a normalized nil value for the union.
This behavior is unexpected, and in my opinion contradicts the definition of the `#shared_nil` tag. My reasoning is that given type definition `T :: struct {}` the value `&T{}` is not a pointer nil value nor a zero value for `^T`
Operating System: macOS 14.5 (23F79)
Odin: dev-2025-01-nightly:2aae4cf
OS: macOS Unknown
CPU: Apple M1 Pro
RAM: 32768 MiB
Backend: LLVM 18.1.8
Expected Behavior
The following program should be able to run to a successful termination
package main
Type_A :: struct { value : int }
Type_B :: struct { }
Union_Of_Pointers :: union #shared_nil {
^Type_A,
^Type_B,
}
main :: proc() {
p : Union_Of_Pointers
assert(p == nil)
p = new(Type_A)
assert(p != nil)
p = new(Type_B)
assert(p != nil)
}
Current Behavior
The first and second assertions checks passes but the third assertion fails where it should not
Steps to Reproduce
1. Copy the code snippet form the expected behavior section into a new odin file
2. run `odin run .`
Comments (2)
2025-02-26 14:02:59 nb-ohad
Might be related to: https://github.com/odin-lang/Odin/issues/1202
2025-08-07 16:21:17 nb-ohad
I did some additional testing and was able to figure out that the issue observed in the code above is not directly related to the `#shared_nil` directive.
It seems the underlaying issue is the `new` operator (using the default allocator) will return a nil value for an allocation of a type which have a size 0. This can be demonstrated via the following code snippet failing the assertion:
package main
Type :: struct { }
main :: proc() {
a : ^Type = new(Type)
assert(a != nil)
}
I see the ability to "allocate" a type of length 0 is a necessity. In my opinion such an allocation should return a pointer pointing to an address but should not hold/capture any space.
Such a behavior is very useful in the following cases (not a comprehensive list):
- The ability to track an allocation/memory without a need to allocate memory for the marker.
- Code-Only shim implementation for existing types
- Usage of an empty struct as a placeholder for future extensions
- Usage of an empty struct as a default type - especially when working with polymorphic code
In all of the above cases, I would expect the code to work exactly the same no matter the size of the type. Failing to do so means that any codebase that need to handle 0 length types will need to implement special handling for such types.