Skip to content

Commit 23d92eb

Browse files
authored
Add Struct.pre_initialize (#15896)
1 parent 7487b31 commit 23d92eb

File tree

7 files changed

+213
-34
lines changed

7 files changed

+213
-34
lines changed

spec/compiler/semantic/primitives_spec.cr

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -451,4 +451,57 @@ describe "Semantic: primitives" do
451451
CRYSTAL
452452
end
453453
end
454+
455+
describe "Struct.pre_initialize" do
456+
def_struct_pre_initialize = <<-CRYSTAL
457+
struct Struct
458+
@[Primitive(:pre_initialize)]
459+
def self.pre_initialize(address : Pointer) : Nil
460+
{% @type %}
461+
end
462+
end
463+
CRYSTAL
464+
465+
it "errors on abstract type" do
466+
assert_error <<-CRYSTAL, "Can't pre-initialize abstract struct Foo"
467+
#{def_struct_pre_initialize}
468+
469+
abstract struct Foo
470+
end
471+
472+
x = 1
473+
Foo.pre_initialize(pointerof(x))
474+
CRYSTAL
475+
end
476+
477+
it "errors on virtual abstract type" do
478+
assert_error <<-CRYSTAL, "Can't pre-initialize abstract struct Foo"
479+
#{def_struct_pre_initialize}
480+
481+
abstract struct Foo
482+
end
483+
484+
struct Bar < Foo
485+
end
486+
487+
x = 1
488+
Bar.as(Foo.class).pre_initialize(pointerof(x))
489+
CRYSTAL
490+
end
491+
492+
it "errors on abstract pointee type" do
493+
assert_error <<-CRYSTAL, "Can't pre-initialize struct using pointer to abstract struct"
494+
#{def_struct_pre_initialize}
495+
496+
abstract struct Foo
497+
end
498+
499+
struct Bar < Foo
500+
end
501+
502+
x = uninitialized Foo
503+
Bar.pre_initialize(pointerof(x))
504+
CRYSTAL
505+
end
506+
end
454507
end

spec/primitives/struct_spec.cr

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
require "spec"
2+
3+
private struct Foo
4+
getter i : Int64
5+
getter str = "abc"
6+
7+
def initialize(@i)
8+
end
9+
10+
def initialize(@str, @i)
11+
end
12+
end
13+
14+
private struct Bar
15+
getter x : UInt8[128]
16+
17+
def initialize(@x)
18+
end
19+
end
20+
21+
private struct Inner
22+
end
23+
24+
private struct Outer
25+
@x = Inner.new
26+
end
27+
28+
describe "Primitives: struct" do
29+
describe ".pre_initialize" do
30+
it "doesn't fail on complex ivar initializer if value is discarded (#14325)" do
31+
bar = uninitialized Outer
32+
Outer.pre_initialize(pointerof(bar))
33+
1
34+
end
35+
36+
it "zeroes the instance data" do
37+
bar = uninitialized Bar
38+
Slice.new(pointerof(bar).as(UInt8*), sizeof(Bar)).fill(0xFF)
39+
Bar.pre_initialize(pointerof(bar))
40+
bar.x.all?(&.zero?).should be_true
41+
end
42+
43+
it "runs inline instance initializers" do
44+
foo = uninitialized Foo
45+
Foo.pre_initialize(pointerof(foo)).should be_nil
46+
foo.str.should eq("abc")
47+
end
48+
49+
it "works when address is on the heap" do
50+
foo_buffer = Pointer(Foo).malloc(1)
51+
Foo.pre_initialize(foo_buffer)
52+
foo_buffer.value.str.should eq("abc")
53+
end
54+
end
55+
end

src/compiler/crystal/codegen/primitives.cr

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -823,14 +823,14 @@ class Crystal::CodeGenVisitor
823823
end
824824

825825
def codegen_primitive_pre_initialize(node, target_def, call_args)
826-
type = node.type
826+
type = context.type.instance_type
827827

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

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

833-
@last = cast_to ptr, type
833+
@last = type.struct? ? llvm_nil : cast_to ptr, type
834834
end
835835

836836
def codegen_primitive_pointer_malloc(node, target_def, call_args)

src/compiler/crystal/interpreter/instructions.cr

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1282,7 +1282,10 @@ require "./repl"
12821282
push: true,
12831283
code: begin
12841284
pointer.clear(size)
1285-
pointer.as(Int32*).value = type_id
1285+
unless type_id == 0
1286+
# 0 stands for any non-reference type
1287+
pointer.as(Int32*).value = type_id
1288+
end
12861289
pointer
12871290
end,
12881291
},

src/compiler/crystal/interpreter/primitives.cr

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -187,10 +187,12 @@ class Crystal::Repl::Compiler
187187
scope.instance_type
188188
end
189189

190-
accept_call_members(node)
190+
accept_call_args(node)
191191

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

195197
initializer_compiled_defs = @context.type_instance_var_initializers(type)
196198
unless initializer_compiled_defs.empty?
@@ -202,6 +204,11 @@ class Crystal::Repl::Compiler
202204
call compiled_def, node: nil
203205
end
204206
end
207+
208+
# `Struct.pre_initialize` does not return a pointer, so always discard it
209+
if !@wants_value || type.struct?
210+
pop(sizeof(Pointer(Void)), node: nil)
211+
end
205212
when "tuple_indexer_known_index"
206213
unless @wants_value
207214
accept_call_members(node)

src/compiler/crystal/semantic/main_visitor.cr

Lines changed: 39 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -2364,17 +2364,22 @@ module Crystal
23642364
end
23652365

23662366
def visit(node : Primitive)
2367+
case node.name
2368+
when "pre_initialize"
2369+
return visit_pre_initialize node
2370+
end
2371+
23672372
# If the method where this primitive is defined has a return type, use it
23682373
if return_type = typed_def.return_type
23692374
node.type = (path_lookup || scope).lookup_type(return_type, free_vars: free_vars)
23702375
return false
23712376
end
23722377

2378+
# TODO: move these into the case expression above and add return types to
2379+
# their corresponding methods
23732380
case node.name
23742381
when "allocate"
23752382
visit_allocate node
2376-
when "pre_initialize"
2377-
visit_pre_initialize node
23782383
when "pointer_malloc"
23792384
visit_pointer_malloc node
23802385
when "pointer_set"
@@ -2478,35 +2483,46 @@ module Crystal
24782483

24792484
case instance_type
24802485
when GenericClassType
2481-
node.raise "Can't pre-initialize instance of generic class #{instance_type} without specifying its type vars"
2486+
node.raise "Can't pre-initialize instance of #{instance_type.type_desc} #{instance_type} without specifying its type vars"
24822487
when UnionType
24832488
node.raise "Can't pre-initialize instance of a union type"
2484-
else
2485-
if instance_type.abstract?
2486-
if instance_type.virtual?
2487-
# This is the same as `.initialize`
2488-
base_type = instance_type.devirtualize
2489+
end
24892490

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

2497-
# This `extra` will replace the Primitive node in CleanupTransformer later on.
2498-
node.extra = extra
2499-
node.type = @program.no_return
2500-
return
2501-
else
2502-
# If the type is not virtual then we know for sure that the type
2503-
# can't be instantiated, and we can produce a compile-time error.
2504-
node.raise "Can't pre-initialize abstract class #{instance_type}"
2505-
end
2496+
extra = Call.new(
2497+
nil,
2498+
"raise",
2499+
StringLiteral.new("Can't pre-initialize abstract #{base_type.type_desc} #{base_type}"),
2500+
global: true).at(node)
2501+
extra.accept self
2502+
2503+
# This `extra` will replace the Primitive node in CleanupTransformer later on.
2504+
node.extra = extra
2505+
node.type = @program.no_return
2506+
return false
2507+
else
2508+
# If the type is not virtual then we know for sure that the type
2509+
# can't be instantiated, and we can produce a compile-time error.
2510+
instance_type = instance_type.devirtualize
2511+
node.raise "Can't pre-initialize abstract #{instance_type.type_desc} #{instance_type}"
25062512
end
2513+
end
25072514

2515+
if instance_type.struct?
2516+
element_type = @vars["address"].type.as(PointerInstanceType).element_type
2517+
if element_type.abstract? && element_type.struct?
2518+
node.raise "Can't pre-initialize struct using pointer to abstract struct"
2519+
end
2520+
node.type = @program.nil_type
2521+
else
25082522
node.type = instance_type
25092523
end
2524+
2525+
false
25102526
end
25112527

25122528
def visit_pointer_malloc(node)

src/primitives.cr

Lines changed: 50 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -58,11 +58,12 @@ class Reference
5858
# overloads. It zeroes the memory, sets up the type ID (necessary for dynamic
5959
# dispatch), and then runs all inline instance variable initializers.
6060
#
61-
# *address* must point to a suitably aligned buffer of at least
62-
# `instance_sizeof(self)` bytes.
61+
# *address* must point to a buffer of at least `instance_sizeof(self)` bytes
62+
# with an alignment of `instance_alignof(self)` or above. `ReferenceStorage`
63+
# fulfils both requirements.
6364
#
64-
# WARNING: This method is unsafe, as it assumes the caller is responsible for
65-
# managing the memory at the given *address* manually.
65+
# WARNING: The caller is responsible for managing the memory at the given
66+
# *address*, in particular if the memory is not garbage-collectable.
6667
#
6768
# ```
6869
# class Foo
@@ -88,7 +89,7 @@ class Reference
8889
# foo.i # => 123
8990
# ```
9091
#
91-
# See also: `Reference.unsafe_construct`.
92+
# See also: `Reference.unsafe_construct`, `Struct.pre_initialize`.
9293
@[Experimental("This API is still under development. Join the discussion about custom reference allocation at [#13481](https://github.com/crystal-lang/crystal/issues/13481).")]
9394
@[Primitive(:pre_initialize)]
9495
{% if compare_versions(Crystal::VERSION, "1.2.0") >= 0 %}
@@ -106,6 +107,50 @@ class Reference
106107
{% end %}
107108
end
108109

110+
struct Struct
111+
# Performs basic initialization so that the given *address* is ready for use
112+
# as an object's instance data.
113+
#
114+
# More specifically, this is the part of object initialization that occurs
115+
# after memory allocation and before calling one of the `#initialize`
116+
# overloads. It zeroes the memory, and then runs all inline instance variable
117+
# initializers.
118+
#
119+
# *address* must point to a buffer of at least `sizeof(self)` bytes with an
120+
# alignment of `alignof(self)` or above. This can for example be a pointer to
121+
# an uninitialized instance.
122+
#
123+
# This method only works for non-virtual constructions. Neither the struct
124+
# type nor *address*'s pointee type can be an abstract struct.
125+
#
126+
# ```
127+
# struct Foo
128+
# getter i : Int64
129+
# getter str = "abc"
130+
#
131+
# def initialize(@i)
132+
# end
133+
# end
134+
#
135+
# foo = uninitialized Foo
136+
# pointerof(foo).to_slice(1).to_unsafe_bytes.fill(0xFF)
137+
# Foo.pre_initialize(pointerof(foo))
138+
# foo # => Foo(@i=0, @str="abc")
139+
# ```
140+
#
141+
# See also: `Reference.pre_initialize`.
142+
@[Experimental("This API is still under development. Join the discussion about custom reference allocation at [#13481](https://github.com/crystal-lang/crystal/issues/13481).")]
143+
@[Primitive(:pre_initialize)]
144+
{% if compare_versions(Crystal::VERSION, "1.2.0") >= 0 %}
145+
def self.pre_initialize(address : Pointer) : Nil
146+
\{% @type %}
147+
end
148+
{% else %}
149+
def self.pre_initialize(address : Pointer) : Nil
150+
end
151+
{% end %}
152+
end
153+
109154
class Class
110155
# :nodoc:
111156
@[Primitive(:class_crystal_instance_type_id)]

0 commit comments

Comments
 (0)