# 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.