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
53 changes: 53 additions & 0 deletions spec/compiler/semantic/primitives_spec.cr
Original file line number Diff line number Diff line change
Expand Up @@ -451,4 +451,57 @@ describe "Semantic: primitives" do
CRYSTAL
end
end

describe "Struct.pre_initialize" do
def_struct_pre_initialize = <<-CRYSTAL
struct Struct
@[Primitive(:pre_initialize)]
def self.pre_initialize(address : Pointer) : Nil
{% @type %}
end
end
CRYSTAL

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

abstract struct Foo
end

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

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

abstract struct Foo
end

struct Bar < Foo
end

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

it "errors on abstract pointee type" do
assert_error <<-CRYSTAL, "Can't pre-initialize struct using pointer to abstract struct"
#{def_struct_pre_initialize}

abstract struct Foo
end

struct Bar < Foo
end

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

private struct Foo
getter i : Int64
getter str = "abc"

def initialize(@i)
end

def initialize(@str, @i)
end
end

private struct Bar
getter x : UInt8[128]

def initialize(@x)
end
end

private struct Inner
end

private struct Outer
@x = Inner.new
end

describe "Primitives: struct" do
describe ".pre_initialize" do
it "doesn't fail on complex ivar initializer if value is discarded (#14325)" do
bar = uninitialized Outer
Outer.pre_initialize(pointerof(bar))
1
end

it "zeroes the instance data" do
bar = uninitialized Bar
Slice.new(pointerof(bar).as(UInt8*), sizeof(Bar)).fill(0xFF)
Bar.pre_initialize(pointerof(bar))
bar.x.all?(&.zero?).should be_true
end

it "runs inline instance initializers" do
foo = uninitialized Foo
Foo.pre_initialize(pointerof(foo)).should be_nil
foo.str.should eq("abc")
end

it "works when address is on the heap" do
foo_buffer = Pointer(Foo).malloc(1)
Foo.pre_initialize(foo_buffer)
foo_buffer.value.str.should eq("abc")
end
end
end
4 changes: 2 additions & 2 deletions src/compiler/crystal/codegen/primitives.cr
Original file line number Diff line number Diff line change
Expand Up @@ -745,14 +745,14 @@ class Crystal::CodeGenVisitor
end

def codegen_primitive_pre_initialize(node, target_def, call_args)
type = node.type
type = context.type.instance_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
@last = type.struct? ? llvm_nil : cast_to ptr, type
end

def codegen_primitive_pointer_malloc(node, target_def, call_args)
Expand Down
5 changes: 4 additions & 1 deletion src/compiler/crystal/interpreter/instructions.cr
Original file line number Diff line number Diff line change
Expand Up @@ -1282,7 +1282,10 @@ require "./repl"
push: true,
code: begin
pointer.clear(size)
pointer.as(Int32*).value = type_id
unless type_id == 0
# 0 stands for any non-reference type
pointer.as(Int32*).value = type_id
end
pointer
end,
},
Expand Down
13 changes: 10 additions & 3 deletions src/compiler/crystal/interpreter/primitives.cr
Original file line number Diff line number Diff line change
Expand Up @@ -187,10 +187,12 @@ class Crystal::Repl::Compiler
scope.instance_type
end

accept_call_members(node)
accept_call_args(node)

dup sizeof(Pointer(Void)), node: nil
reset_class(aligned_instance_sizeof_type(type), type_id(type), node: node)
# 0 stands for any non-reference type in `reset_class`
# (normally 0 stands for `Nil` so there is no conflict here)
type_id = type.struct? ? 0 : type_id(type)
reset_class(aligned_instance_sizeof_type(type), type_id, node: node)

initializer_compiled_defs = @context.type_instance_var_initializers(type)
unless initializer_compiled_defs.empty?
Expand All @@ -202,6 +204,11 @@ class Crystal::Repl::Compiler
call compiled_def, node: nil
end
end

# `Struct.pre_initialize` does not return a pointer, so always discard it
if !@wants_value || type.struct?
pop(sizeof(Pointer(Void)), node: nil)
end
when "tuple_indexer_known_index"
unless @wants_value
accept_call_members(node)
Expand Down
62 changes: 39 additions & 23 deletions src/compiler/crystal/semantic/main_visitor.cr
Original file line number Diff line number Diff line change
Expand Up @@ -2364,17 +2364,22 @@ module Crystal
end

def visit(node : Primitive)
case node.name
when "pre_initialize"
return visit_pre_initialize node
end

# If the method where this primitive is defined has a return type, use it
if return_type = typed_def.return_type
node.type = (path_lookup || scope).lookup_type(return_type, free_vars: free_vars)
return false
end

# TODO: move these into the case expression above and add return types to
# their corresponding methods
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 @@ -2478,35 +2483,46 @@ module Crystal

case instance_type
when GenericClassType
node.raise "Can't pre-initialize instance of generic class #{instance_type} without specifying its type vars"
node.raise "Can't pre-initialize instance of #{instance_type.type_desc} #{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
end

extra = Call.new(
nil,
"raise",
StringLiteral.new("Can't pre-initialize abstract class #{base_type}"),
global: true).at(node)
extra.accept self
if instance_type.abstract?
if instance_type.virtual? && !instance_type.struct?
# This is the same as `.initialize`
base_type = instance_type.devirtualize

# 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
extra = Call.new(
nil,
"raise",
StringLiteral.new("Can't pre-initialize abstract #{base_type.type_desc} #{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 false
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.
instance_type = instance_type.devirtualize
node.raise "Can't pre-initialize abstract #{instance_type.type_desc} #{instance_type}"
end
end

if instance_type.struct?
element_type = @vars["address"].type.as(PointerInstanceType).element_type
if element_type.abstract? && element_type.struct?
node.raise "Can't pre-initialize struct using pointer to abstract struct"
end
node.type = @program.nil_type
else
node.type = instance_type
end

false
end

def visit_pointer_malloc(node)
Expand Down
55 changes: 50 additions & 5 deletions src/primitives.cr
Original file line number Diff line number Diff line change
Expand Up @@ -58,11 +58,12 @@ class Reference
# 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.
# *address* must point to a buffer of at least `instance_sizeof(self)` bytes
# with an alignment of `instance_alignof(self)` or above. `ReferenceStorage`
# fulfils both requirements.
#
# WARNING: This method is unsafe, as it assumes the caller is responsible for
# managing the memory at the given *address* manually.
# WARNING: The caller is responsible for managing the memory at the given
# *address*, in particular if the memory is not garbage-collectable.
#
# ```
# class Foo
Expand All @@ -88,7 +89,7 @@ class Reference
# foo.i # => 123
# ```
#
# See also: `Reference.unsafe_construct`.
# See also: `Reference.unsafe_construct`, `Struct.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).")]
@[Primitive(:pre_initialize)]
{% if compare_versions(Crystal::VERSION, "1.2.0") >= 0 %}
Expand All @@ -106,6 +107,50 @@ class Reference
{% end %}
end

struct Struct
# Performs basic initialization so that the given *address* is ready for use
# as an object's instance data.
#
# 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, and then runs all inline instance variable
# initializers.
#
# *address* must point to a buffer of at least `sizeof(self)` bytes with an
# alignment of `alignof(self)` or above. This can for example be a pointer to
# an uninitialized instance.
#
# This method only works for non-virtual constructions. Neither the struct
# type nor *address*'s pointee type can be an abstract struct.
#
# ```
# struct Foo
# getter i : Int64
# getter str = "abc"
#
# def initialize(@i)
# end
# end
#
# foo = uninitialized Foo
# pointerof(foo).to_slice(1).to_unsafe_bytes.fill(0xFF)
# Foo.pre_initialize(pointerof(foo))
# foo # => Foo(@i=0, @str="abc")
# ```
#
# 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).")]
@[Primitive(:pre_initialize)]
{% if compare_versions(Crystal::VERSION, "1.2.0") >= 0 %}
def self.pre_initialize(address : Pointer) : Nil
\{% @type %}
end
{% else %}
def self.pre_initialize(address : Pointer) : Nil
end
{% end %}
end

class Class
# :nodoc:
@[Primitive(:class_crystal_instance_type_id)]
Expand Down