Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 62 additions & 0 deletions spec/compiler/semantic/primitives_spec.cr
Original file line number Diff line number Diff line change
Expand Up @@ -331,4 +331,66 @@ describe "Semantic: primitives" do
end
end
end

describe "Reference.pre_initialize" do
def_reference_pre_initialize = <<-CRYSTAL
class Reference
@[Primitive(:pre_initialize)]
def self.pre_initialize(address : Pointer)
{% @type %}
end
end
CRYSTAL

it "types with reference type" do
assert_type(<<-CRYSTAL) { types["Foo"] }
#{def_reference_pre_initialize}

class Foo
end

x = 1
Foo.pre_initialize(pointerof(x))
CRYSTAL
end

it "types with virtual reference type" do
assert_type(<<-CRYSTAL) { types["Foo"].virtual_type! }
#{def_reference_pre_initialize}

class Foo
end

class Bar < Foo
end

x = 1
Bar.as(Foo.class).pre_initialize(pointerof(x))
CRYSTAL
end

it "errors on uninstantiated generic type" do
assert_error <<-CRYSTAL, "Can't pre-initialize instance of generic class Foo(T) without specifying its type vars"
#{def_reference_pre_initialize}

class Foo(T)
end

x = 1
Foo.pre_initialize(pointerof(x))
CRYSTAL
end

it "errors on abstract type" do
assert_error <<-CRYSTAL, "Can't pre-initialize abstract class Foo"
#{def_reference_pre_initialize}

abstract class Foo
end

x = 1
Foo.pre_initialize(pointerof(x))
CRYSTAL
end
end
end
85 changes: 85 additions & 0 deletions spec/primitives/reference_spec.cr
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
require "spec"

private abstract class Base
end

private class Foo < Base
getter i : Int64
getter str = "abc"

def initialize(@i)
end

def initialize(@str, @i)
end
end

private class Bar < Base
getter x : UInt8[128]

def initialize(@x)
end
end

describe "Primitives: reference" do
describe ".pre_initialize" do
it "zeroes the instance data" do
bar_buffer = GC.malloc(instance_sizeof(Bar))
Slice.new(bar_buffer.as(UInt8*), instance_sizeof(Bar)).fill(0xFF)
bar = Bar.pre_initialize(bar_buffer)
bar.x.all?(&.zero?).should be_true
end

it "sets type ID" do
foo_buffer = GC.malloc(instance_sizeof(Foo))
base = Foo.pre_initialize(foo_buffer).as(Base)
base.crystal_type_id.should eq(Foo.crystal_instance_type_id)
end

it "runs inline instance initializers" do
foo_buffer = GC.malloc(instance_sizeof(Foo))
foo = Foo.pre_initialize(foo_buffer)
foo.str.should eq("abc")
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 = 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")
end

# see notes in `Reference.pre_initialize`
{% if compare_versions(Crystal::VERSION, "1.2.0") >= 0 %}
it "works with virtual type" do
foo_buffer = GC.malloc(instance_sizeof(Foo))
foo = Foo.as(Base.class).pre_initialize(foo_buffer).should be_a(Foo)
foo.str.should eq("abc")
end
{% else %}
pending! "works with virtual type"
{% end %}

it "raises on abstract virtual type" do
expect_raises(Exception, "Can't pre-initialize abstract class Base") do
Base.as(Base.class).pre_initialize(Pointer(Void).null)
end
end
end

describe ".unsafe_construct" do
it "constructs an object in-place" do
foo_buffer = GC.malloc(instance_sizeof(Foo))
foo = Foo.unsafe_construct(foo_buffer, 123_i64)
foo.i.should eq(123)
foo.str.should eq("abc")

str = String.build &.<< "def"
foo = Foo.unsafe_construct(foo_buffer, str, 789_i64)
foo.i.should eq(789)
foo.str.should be(str)
end
end
end
12 changes: 8 additions & 4 deletions src/compiler/crystal/codegen/codegen.cr
Original file line number Diff line number Diff line change
Expand Up @@ -2032,15 +2032,19 @@ module Crystal
end
end

memset type_ptr, int8(0), struct_type.size
run_instance_vars_initializers(type, type, type_ptr)
pre_initialize_aggregate(type, struct_type, type_ptr)
end

def pre_initialize_aggregate(type, struct_type, ptr)
memset ptr, int8(0), struct_type.size
run_instance_vars_initializers(type, type, ptr)

unless type.struct?
type_id_ptr = aggregate_index(struct_type, type_ptr, 0)
type_id_ptr = aggregate_index(struct_type, ptr, 0)
store type_id(type), type_id_ptr
end

@last = type_ptr
@last = ptr
end

def allocate_tuple(type, &)
Expand Down
13 changes: 13 additions & 0 deletions src/compiler/crystal/codegen/primitives.cr
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ class Crystal::CodeGenVisitor
codegen_primitive_convert node, target_def, call_args, checked: false
when "allocate"
codegen_primitive_allocate node, target_def, call_args
when "pre_initialize"
codegen_primitive_pre_initialize node, target_def, call_args
when "pointer_malloc"
codegen_primitive_pointer_malloc node, target_def, call_args
when "pointer_set"
Expand Down Expand Up @@ -740,6 +742,17 @@ class Crystal::CodeGenVisitor
@last
end

def codegen_primitive_pre_initialize(node, target_def, call_args)
type = node.type

base_type = type.is_a?(VirtualType) ? type.base_type : type

ptr = call_args[target_def.owner.passed_as_self? ? 1 : 0]
pre_initialize_aggregate base_type, llvm_struct_type(base_type), ptr

@last = cast_to ptr, type
end

def codegen_primitive_pointer_malloc(node, target_def, call_args)
type = node.type.as(PointerInstanceType)
llvm_type = llvm_embedded_type(type.element_type)
Expand Down
2 changes: 1 addition & 1 deletion src/compiler/crystal/semantic/cleanup_transformer.cr
Original file line number Diff line number Diff line change
Expand Up @@ -1052,7 +1052,7 @@ module Crystal
# For `allocate` on a virtual abstract type we make `extra`
# be a call to `raise` at runtime. Here we just replace the
# "allocate" primitive with that raise call.
if node.name == "allocate" && extra
if node.name.in?("allocate", "pre_initialize") && extra
return extra
end

Expand Down
38 changes: 38 additions & 0 deletions src/compiler/crystal/semantic/main_visitor.cr
Original file line number Diff line number Diff line change
Expand Up @@ -2292,6 +2292,8 @@ module Crystal
case node.name
when "allocate"
visit_allocate node
when "pre_initialize"
visit_pre_initialize node
when "pointer_malloc"
visit_pointer_malloc node
when "pointer_set"
Expand Down Expand Up @@ -2388,6 +2390,42 @@ module Crystal
end
end

def visit_pre_initialize(node)
instance_type = scope.instance_type

case instance_type
when GenericClassType
node.raise "Can't pre-initialize instance of generic class #{instance_type} without specifying its type vars"
when UnionType
node.raise "Can't pre-initialize instance of a union type"
else
if instance_type.abstract?
if instance_type.virtual?
# This is the same as `.initialize`
base_type = instance_type.devirtualize

extra = Call.new(
nil,
"raise",
StringLiteral.new("Can't pre-initialize abstract class #{base_type}"),
global: true).at(node)
extra.accept self

# This `extra` will replace the Primitive node in CleanupTransformer later on.
node.extra = extra
node.type = @program.no_return
return
else
# If the type is not virtual then we know for sure that the type
# can't be instantiated, and we can produce a compile-time error.
node.raise "Can't pre-initialize abstract class #{instance_type}"
end
end

node.type = instance_type
end
end

def visit_pointer_malloc(node)
if scope.instance_type.is_a?(GenericClassType)
node.raise "can't malloc pointer without type, use Pointer(Type).malloc(size)"
Expand Down
55 changes: 55 additions & 0 deletions src/primitives.cr
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,61 @@ class Reference
@[Primitive(:object_id)]
def object_id : UInt64
end

# Performs basic initialization so that the given *address* is ready for use
# as an object's instance data. Returns *address* cast to `self`'s type.
#
# More specifically, this is the part of object initialization that occurs
# after memory allocation and before calling one of the `#initialize`
# overloads. It zeroes the memory, sets up the type ID (necessary for dynamic
# dispatch), and then runs all inline instance variable initializers.
#
# *address* must point to a suitably aligned buffer of at least
# `instance_sizeof(self)` bytes.
#
# WARNING: This method is unsafe, as it assumes the caller is responsible for
# managing the memory at the given *address* manually.
#
# ```
# class Foo
# getter i : Int64
# getter str = "abc"
#
# def initialize(@i)
# end
#
# def self.alloc_with_libc(i : Int64)
# foo_buffer = LibC.malloc(instance_sizeof(Foo))
# foo = Foo.pre_initialize(foo_buffer)
# foo.i # => 0
# foo.str # => "abc"
# (foo || "").is_a?(Foo) # => true
#
# foo.initialize(i) # okay
# foo
# end
# end
#
# foo = Foo.alloc_with_libc(123_i64)
# foo.i # => 123
# ```
#
# See also: `Reference.unsafe_construct`.
@[Experimental("This API is still under development. Join the discussion about custom reference allocation at [#13481](https://github.com/crystal-lang/crystal/issues/13481).")]
@[Primitive(:pre_initialize)]
{% if compare_versions(Crystal::VERSION, "1.2.0") >= 0 %}
def self.pre_initialize(address : Pointer)
# This ensures `.pre_initialize` is instantiated for every subclass,
# otherwise all calls will refer to the sole instantiation in
# `Reference.class`. This is necessary when the receiver is a virtual
# metaclass type. Apparently this works even for primitives
\{% @type %}
end
{% else %}
# Primitives cannot have a body until 1.2.0 (#11147)
def self.pre_initialize(address : Pointer)
end
{% end %}
end

class Class
Expand Down
43 changes: 43 additions & 0 deletions src/reference.cr
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,49 @@
# The instance's memory is automatically freed (garbage-collected) when
# the instance is no longer referred by any other entity in the program.
class Reference
# Constructs an object in-place at the given *address*, forwarding *args* and
# *opts* to `#initialize`. Returns that object.
#
# This method can be used to decouple object allocation from initialization.
# For example, the instance data might come from a custom allocator, or it
# might reside on the stack using a type like `ReferenceStorage`.
#
# *address* must point to a suitably aligned buffer of at least
# `instance_sizeof(self)` bytes.
#
# WARNING: This method is unsafe, as it assumes the caller is responsible for
# managing the memory at the given *address* manually.
#
# ```
# class Foo
# getter i : Int64
# getter str = "abc"
#
# def initialize(@i)
# end
#
# def finalize
# puts "bye"
# end
# end
#
# foo_buffer = uninitialized ReferenceStorage(Foo)
# foo = Foo.unsafe_construct(pointerof(foo_buffer), 123_i64)
# begin
# foo # => #<Foo:0x... @i=123, @str="abc">
# ensure
# foo.finalize if foo.responds_to?(:finalize) # prints "bye"
# end
# ```
#
# See also: `Reference.pre_initialize`.
@[Experimental("This API is still under development. Join the discussion about custom reference allocation at [#13481](https://github.com/crystal-lang/crystal/issues/13481).")]
def self.unsafe_construct(address : Pointer, *args, **opts) : self
obj = pre_initialize(address)
obj.initialize(*args, **opts)
obj
end

# Returns `true` if this reference is the same as *other*. Invokes `same?`.
def ==(other : self)
same?(other)
Expand Down