diff --git a/spec/compiler/semantic/primitives_spec.cr b/spec/compiler/semantic/primitives_spec.cr index 34b8b3a29501..973d356e2df8 100644 --- a/spec/compiler/semantic/primitives_spec.cr +++ b/spec/compiler/semantic/primitives_spec.cr @@ -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 diff --git a/spec/primitives/struct_spec.cr b/spec/primitives/struct_spec.cr new file mode 100644 index 000000000000..e635e1e5f0f6 --- /dev/null +++ b/spec/primitives/struct_spec.cr @@ -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 diff --git a/src/compiler/crystal/codegen/primitives.cr b/src/compiler/crystal/codegen/primitives.cr index 7eedfdab4adf..d066b0eb6c53 100644 --- a/src/compiler/crystal/codegen/primitives.cr +++ b/src/compiler/crystal/codegen/primitives.cr @@ -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) diff --git a/src/compiler/crystal/interpreter/instructions.cr b/src/compiler/crystal/interpreter/instructions.cr index f8f986f1b44a..43b43ba6fe9d 100644 --- a/src/compiler/crystal/interpreter/instructions.cr +++ b/src/compiler/crystal/interpreter/instructions.cr @@ -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, }, diff --git a/src/compiler/crystal/interpreter/primitives.cr b/src/compiler/crystal/interpreter/primitives.cr index 619f678ad6bd..44538ea800fb 100644 --- a/src/compiler/crystal/interpreter/primitives.cr +++ b/src/compiler/crystal/interpreter/primitives.cr @@ -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? @@ -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) diff --git a/src/compiler/crystal/semantic/main_visitor.cr b/src/compiler/crystal/semantic/main_visitor.cr index bccbe5d601e1..be2329e2ab73 100644 --- a/src/compiler/crystal/semantic/main_visitor.cr +++ b/src/compiler/crystal/semantic/main_visitor.cr @@ -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" @@ -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) diff --git a/src/primitives.cr b/src/primitives.cr index 4f93fac83af3..d5664383f8cb 100644 --- a/src/primitives.cr +++ b/src/primitives.cr @@ -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 @@ -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 %} @@ -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)]