Skip to content

Commit

Permalink
Add ReferenceStorage for manual allocation of references (crystal-l…
Browse files Browse the repository at this point in the history
…ang#14106)

Co-authored-by: Sijawusz Pur Rahnama <sija@sija.pl>
  • Loading branch information
HertzDevil and Sija committed Dec 23, 2023
1 parent a3bedf9 commit 15010d3
Show file tree
Hide file tree
Showing 7 changed files with 132 additions and 5 deletions.
40 changes: 40 additions & 0 deletions spec/compiler/semantic/reference_storage_spec.cr
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
require "../../spec_helper"

describe "Semantic: ReferenceStorage" do
it "errors if T is a struct type" do
assert_error <<-CRYSTAL, "Can't instantiate ReferenceStorage(T) with T = Foo (T must be a reference type)"
struct Foo
@x = 1
end
ReferenceStorage(Foo)
CRYSTAL
end

it "errors if T is a value type" do
assert_error <<-CRYSTAL, "Can't instantiate ReferenceStorage(T) with T = Int32 (T must be a reference type)"
ReferenceStorage(Int32)
CRYSTAL
end

it "errors if T is a union type" do
assert_error <<-CRYSTAL, "Can't instantiate ReferenceStorage(T) with T = (Bar | Foo) (T must be a reference type)"
class Foo
end
class Bar
end
ReferenceStorage(Foo | Bar)
CRYSTAL
end

it "errors if T is a nilable type" do
assert_error <<-CRYSTAL, "Can't instantiate ReferenceStorage(T) with T = (Foo | Nil) (T must be a reference type)"
class Foo
end
ReferenceStorage(Foo?)
CRYSTAL
end
end
4 changes: 1 addition & 3 deletions spec/primitives/reference_spec.cr
Original file line number Diff line number Diff line change
Expand Up @@ -43,9 +43,7 @@ describe "Primitives: reference" do
end

it "works when address is on the stack" do
# suitably aligned, sufficient storage for type ID + i64 + ptr
# TODO: use `ReferenceStorage` instead
foo_buffer = uninitialized UInt64[3]
foo_buffer = uninitialized ReferenceStorage(Foo)
foo = Foo.pre_initialize(pointerof(foo_buffer))
pointerof(foo_buffer).as(typeof(Foo.crystal_instance_type_id)*).value.should eq(Foo.crystal_instance_type_id)
foo.str.should eq("abc")
Expand Down
4 changes: 4 additions & 0 deletions src/compiler/crystal/codegen/llvm_typer.cr
Original file line number Diff line number Diff line change
Expand Up @@ -245,6 +245,10 @@ module Crystal
llvm_type(type.remove_alias, wants_size)
end

private def create_llvm_type(type : ReferenceStorageType, wants_size)
llvm_struct_type(type.reference_type, wants_size)
end

private def create_llvm_type(type : NonGenericModuleType | GenericClassType, wants_size)
# This can only be reached if the module or generic class don't have implementors
@llvm_context.int1
Expand Down
6 changes: 5 additions & 1 deletion src/compiler/crystal/program.cr
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,10 @@ module Crystal
types["Struct"] = struct_t = @struct_t = NonGenericClassType.new self, self, "Struct", value
abstract_value_type(struct_t)

types["ReferenceStorage"] = @reference_storage = reference_storage = GenericReferenceStorageType.new self, self, "ReferenceStorage", value, ["T"]
reference_storage.declare_instance_var("@type_id", int32)
reference_storage.can_be_stored = false

types["Enumerable"] = @enumerable = GenericModuleType.new self, self, "Enumerable", ["T"]
types["Indexable"] = @indexable = GenericModuleType.new self, self, "Indexable", ["T"]

Expand Down Expand Up @@ -493,7 +497,7 @@ module Crystal

{% for name in %w(object no_return value number reference void nil bool char int int8 int16 int32 int64 int128
uint8 uint16 uint32 uint64 uint128 float float32 float64 string symbol pointer enumerable indexable
array static_array exception tuple named_tuple proc union enum range regex crystal
array static_array reference_storage exception tuple named_tuple proc union enum range regex crystal
packed_annotation thread_local_annotation no_inline_annotation
always_inline_annotation naked_annotation returns_twice_annotation
raises_annotation primitive_annotation call_convention_annotation
Expand Down
28 changes: 27 additions & 1 deletion src/compiler/crystal/types.cr
Original file line number Diff line number Diff line change
Expand Up @@ -550,7 +550,7 @@ module Crystal
def allows_instance_vars?
case self
when program.object, program.value, program.struct,
program.reference, program.class_type,
program.reference, program.class_type, program.reference_storage,
program.number, program.int, program.float,
program.tuple, program.named_tuple,
program.pointer, program.static_array,
Expand Down Expand Up @@ -2642,6 +2642,32 @@ module Crystal
end
end

# The non-instantiated ReferenceStorage(T) type.
class GenericReferenceStorageType < GenericClassType
@struct = true

def new_generic_instance(program, generic_type, type_vars)
t = type_vars["T"].type

unless t.is_a?(TypeParameter) || (t.class? && !t.struct?)
raise TypeException.new "Can't instantiate ReferenceStorage(T) with T = #{t} (T must be a reference type)"
end

ReferenceStorageType.new program, t
end
end

class ReferenceStorageType < GenericClassInstanceType
getter reference_type : Type

def initialize(program, @reference_type)
t_var = Var.new("T", @reference_type)
t_var.bind_to t_var

super(program, program.reference_storage, program.struct, {"T" => t_var} of String => ASTNode)
end
end

# A lib type, like `lib LibC`.
class LibType < ModuleType
getter link_annotations : Array(LinkAnnotation)?
Expand Down
1 change: 1 addition & 0 deletions src/prelude.cr
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ require "raise"
require "random"
require "range"
require "reference"
require "reference_storage"
require "regex"
require "set"
{% unless flag?(:wasm32) %}
Expand Down
54 changes: 54 additions & 0 deletions src/reference_storage.cr
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
# a `ReferenceStorage(T)` provides the minimum storage for the instance data of
# an object of type `T`. The compiler guarantees that
# `sizeof(ReferenceStorage(T)) == instance_sizeof(T)` and
# `alignof(ReferenceStorage(T)) == instance_alignof(T)` always hold, which means
# `Pointer(ReferenceStorage(T))` and `T` are binary-compatible.
#
# `T` must be a non-union reference type.
#
# WARNING: `ReferenceStorage` is only necessary for manual memory management,
# such as creating instances of `T` with a non-default allocator. Therefore,
# this type is unsafe and no public constructors are defined.
#
# WARNING: `ReferenceStorage` is unsuitable when instances of `T` require more
# than `instance_sizeof(T)` bytes, such as `String` and `Log::Metadata`.
@[Experimental("This type's API is still under development. Join the discussion about custom reference allocation at [#13481](https://github.com/crystal-lang/crystal/issues/13481).")]
struct ReferenceStorage(T)
private def initialize
end

# Returns whether `self` and *other* are bytewise equal.
#
# NOTE: This does not call `T#==`, so it works even if `self` or *other* does
# not represent a valid instance of `T`. If validity is guaranteed, call
# `to_reference == other.to_reference` instead to use `T#==`.
def ==(other : ReferenceStorage(T)) : Bool
to_bytes == other.to_bytes
end

def ==(other) : Bool
false
end

def hash(hasher)
to_bytes.hash(hasher)
end

def to_s(io : IO) : Nil
io << "ReferenceStorage(#<" << T << ":0x"
pointerof(@type_id).address.to_s(io, 16)
io << ">)"
end

# Returns a `T` whose instance data refers to `self`.
#
# WARNING: The caller is responsible for ensuring that the instance data is
# correctly initialized and outlives the returned `T`.
def to_reference : T
pointerof(@type_id).as(T)
end

protected def to_bytes
Slice.new(pointerof(@type_id).as(UInt8*), instance_sizeof(T))
end
end

0 comments on commit 15010d3

Please sign in to comment.