# Julep: Redesigning `Array` using a new lower-level container type
# new playground
```julia=
primitive type InteriorPtr sizeof(Ptr) end # Addrspace(Loaded)
mutable struct ForeignMemory{T, atomic::Bool} <: AbstractVector{T}
@atomic length::Int
const data::InteriorPtr # => Vararg{T,length}
# const types::IfElse{isbitsunion(T), InteriorPtr, Nothing} # => Vararg{UInt8,length}
const owner::Any
end
mutable struct Memory{T, atomic::Bool} <: AbstractVector{T}
@atomic length::Int
# ::Vararg{T,length}
# ::IfElse{isbitsunion(T), Vararg{Uint8,length}, Nothing}
end
struct MemoryRef{T, atomic::Bool} <: Ref{T}
ref::Union{Memory{T, atomic}, ForeignMemory{T, atomic}}
data::InteriorPtr
# types::IfElse{isbitsunion(T), InteriorPtr, Nothing}
end
mutable struct Array{T,N} <: AbstractArray{T,N}
mem::MemoryRef{T}
size::Dims{N} # NTuple{N,Int}
end
function pointer(ref::Memory)
return pointer_from_objref(ref) + 8
end
function pointer(ref::ForeignMemory)
return ref.data
end
function pointer(mem::MemoryRef)
return mem.data
end
function length(mem::MemoryRef)
return mem.ref.length
end
function MemoryRef(ref::Memory)
return new(ref, pointer(ref), pointer(ref) + ref.length)
end
function MemoryRef(ref::ForeignMemory)
return new(ref, pointer(ref), ref.types)
end
function memoryindex(mem::MemoryRef, idx::Int, [boundscheck::Bool]) <: Core.Builtin
# @boundscheck (mem.data - pointer(mem)) / elsize(mem) + idx - 1 < length(mem) || throw(BoundsError(mem, idx))
# newdata = mem.data + (idx - 1) * elsize(mem)
# return MemoryRef{T}(
# mem.ref,
# newdata,
# mem.types === nothing ? nothing : mem.types + idx)
end
function memoryref(mem::MemoryRef{T}, order::Symbol=:not_atomic, boundscheck::Bool) <: Core.Builtin
# @boundscheck isempty(mem) && throw(BoundsError(mem))
# mem.types === nothing || (T = select(T, load(mem.types)))
# return GC.@preserve mem.ref load(mem.data, order)::T
end
function memoryset(mem::MemoryRef{T}, x, order::Symbol=:not_atomic, boundscheck::Bool) <: Core.Builtin
# @boundscheck isempty(mem) && throw(BoundsError(mem))
# x = x::T
# mem.types === nothing || store!(mem.types, T)
# GC.@preserve mem.ref store!(mem.data, x, order)
# GC.wb(memoryowner(mem), x)
# return x or return mem
end
function length(arr::Vector)
return arr.size[1]
end
function length(arr::Array)
return length(arr.mem)
end
function arrayref([boundscheck::Bool], arr::Array{T}, idx::Int)
@boundscheck unsigned(idx - 1) < unsigned(length(arr)) || throw(BoundsError(arr, idx))
mem = @inbounds memoryidx(arr.mem, idx)
return memoryref(mem)
end
```
Note that `MemoryRef{T}` here is always a valid index (unlike C/C++/LLVM GEP which lets it point one byte past the end), except when `MemoryRef{T}` is empty. This always-valid property corresponds to Julia's use of inclusive one-based indexing, while zero-based indexing normally requires the existence and use of invalid indexes which leads to memory model violations and undefined behavior. Hence, one-based indexing is superior, since it does not lead to the need for undefined behavior to be a primary characteristic of the memory model, like zero-based indexing does in C.
The `InteriorPtr` value is currently `Ptr{Cvoid}` for most arrays, but an `Int` index for bitsunion and ghost value (elsize=0) elements.