@@ -14,25 +14,13 @@ require "crystal/spin_lock"
14
14
module Crystal
15
15
# :nodoc:
16
16
module Once
17
- enum State : Int8
18
- Processing = -1
19
- Uninitialized = 0
20
- Initialized = 1
21
- end
22
-
23
- {% if compare_versions(Crystal ::VERSION , " 1.16.0-dev" ) >= 0 % }
24
- alias FlagT = State
25
- {% else % }
26
- alias FlagT = Bool
27
- {% end % }
28
-
29
17
struct Operation
30
18
include PointerLinkedList ::Node
31
19
32
20
getter fiber : Fiber
33
- getter flag : FlagT *
21
+ getter flag : Bool *
34
22
35
- def initialize (@flag : FlagT * , @fiber : Fiber )
23
+ def initialize (@flag : Bool * , @fiber : Fiber )
36
24
@waiting = PointerLinkedList (Fiber ::PointerLinkedListNode ).new
37
25
end
38
26
@@ -53,126 +41,105 @@ module Crystal
53
41
@@operations = PointerLinkedList (Operation ).new
54
42
end
55
43
56
- protected def self.exec (flag : FlagT * , & )
44
+ protected def self.exec (flag : Bool * , & )
57
45
@@spin .lock
58
46
59
- exec_impl(flag) { yield }
47
+ if flag.value
48
+ @@spin .unlock
49
+ elsif operation = processing?(flag)
50
+ check_reentrancy(operation)
51
+ wait_initializer(operation)
52
+ else
53
+ run_initializer(flag) { yield }
54
+ end
60
55
61
56
# safety check, and allows to safely call `Intrinsics.unreachable` in
62
57
# `__crystal_once`
63
- if flag.is_a?(State * )
64
- return if flag.value.initialized?
65
- else
66
- return if flag.value
67
- end
58
+ return if flag.value
68
59
69
- System .print_error " BUG: failed to initialize constant or class variable\n "
60
+ System .print_error " BUG: failed to initialize class variable or constant \n "
70
61
LibC ._exit(1 )
71
62
end
72
63
73
- private def self.run_initializer (flag , & )
74
- if flag.is_a?( State * )
75
- flag .value = State :: Processing
64
+ private def self.processing? (flag )
65
+ @@operations .each do | operation |
66
+ return operation if operation .value.flag == flag
76
67
end
68
+ end
69
+
70
+ private def self.check_reentrancy (operation )
71
+ if operation.value.fiber == Fiber .current
72
+ @@spin .unlock
73
+ raise " Recursion while initializing class variables and/or constants"
74
+ end
75
+ end
76
+
77
+ private def self.wait_initializer (operation )
78
+ waiting = Fiber ::PointerLinkedListNode .new(Fiber .current)
79
+ operation.value.add_waiter(pointerof (waiting))
80
+ @@spin .unlock
81
+ Fiber .suspend
82
+ end
83
+
84
+ private def self.run_initializer (flag , & )
77
85
operation = Operation .new(flag, Fiber .current)
78
86
@@operations .push pointerof (operation)
79
87
@@spin .unlock
80
88
81
89
yield
82
90
83
91
@@spin .lock
84
- if flag.is_a?(State * )
85
- flag.value = State ::Initialized
86
- else
87
- flag.value = true
88
- end
92
+ flag.value = true
89
93
@@operations .delete pointerof (operation)
90
94
@@spin .unlock
91
95
92
96
operation.resume_all
93
97
end
94
-
95
- # Searches if a fiber is already running the initializer, in which case it
96
- # checks for reentrancy then suspends the fiber until the value is ready and
97
- # returns true; otherwise immediately returns false.
98
- private def self.wait_initializer? (flag ) : Bool
99
- @@operations .each do |operation |
100
- next unless operation.value.flag == flag
101
-
102
- current_fiber = Fiber .current
103
-
104
- if operation.value.fiber == current_fiber
105
- @@spin .unlock
106
- raise " Recursion while initializing class variables and/or constants"
107
- end
108
-
109
- waiting = Fiber ::PointerLinkedListNode .new(current_fiber)
110
- operation.value.add_waiter(pointerof (waiting))
111
- @@spin .unlock
112
-
113
- Fiber .suspend
114
- return true
115
- end
116
-
117
- false
118
- end
119
98
end
120
99
121
100
# :nodoc:
101
+ #
102
+ # Never inlined to avoid bloating the call site with the slow-path that should
103
+ # usually not be taken.
122
104
@[NoInline ]
123
- def self.once (flag : Once :: FlagT * , initializer : Void * )
105
+ def self.once (flag : Bool * , initializer : Void * )
124
106
Once .exec(flag, & Proc (Nil ).new(initializer, Pointer (Void ).null))
125
107
end
126
- end
127
-
128
- {% if compare_versions(Crystal ::VERSION , " 1.16.0-dev" ) >= 0 % }
129
- module Crystal
130
- module Once
131
- private def self.exec_impl (flag , & )
132
- case flag.value
133
- in .initialized?
134
- @@spin .unlock
135
- return
136
- in .uninitialized?
137
- run_initializer(flag) { yield }
138
- in .processing?
139
- raise " unreachable" unless wait_initializer?(flag)
140
- end
141
- end
142
- end
143
108
144
- # :nodoc:
145
- def self.once (flag : Once ::State * , & )
146
- Once .exec(flag) { yield } unless flag.value.initialized?
147
- end
109
+ # :nodoc:
110
+ #
111
+ # NOTE: should also never be inlined, but that would capture the block, which
112
+ # would be a breaking change when we use this method to protect class getter
113
+ # and class property macros with lazy initialization (the block may return or
114
+ # break).
115
+ #
116
+ # TODO: consider a compile time flag to enable/disable the capture? returning
117
+ # from the block is unexpected behavior: the returned value won't be saved in
118
+ # the class variable.
119
+ def self.once (flag : Bool * , & )
120
+ Once .exec(flag) { yield } unless flag.value
148
121
end
122
+ end
149
123
124
+ {% if compare_versions(Crystal ::VERSION , " 1.16.0-dev" ) >= 0 % }
150
125
# :nodoc:
126
+ #
127
+ # We always inline this accessor to optimize for the fast-path (already
128
+ # initialized).
151
129
@[AlwaysInline ]
152
- fun __crystal_once (flag : Crystal :: Once :: State * , initializer : Void * )
153
- return if flag.value.initialized?
130
+ fun __crystal_once (flag : Bool * , initializer : Void * )
131
+ return if flag.value
154
132
Crystal .once(flag, initializer)
155
- Intrinsics .unreachable unless flag.value.initialized?
156
- end
157
- {% else % }
158
- module Crystal
159
- module Once
160
- private def self.exec_impl (flag , & )
161
- if flag.value
162
- @@spin .unlock
163
- elsif ! wait_initializer?(flag)
164
- run_initializer(flag) { yield }
165
- end
166
- end
167
- end
168
133
169
- # :nodoc:
170
- def self.once ( flag : Bool * , & )
171
- Once .exec(flag) { yield } unless flag.value
172
- end
134
+ # tells LLVM to assume that the flag is true, this avoids repeated access to
135
+ # the same constant or class variable to check the flag and try to run the
136
+ # initializer (only the first access will)
137
+ Intrinsics .unreachable unless flag.value
173
138
end
174
-
139
+ { % else % }
175
140
# :nodoc:
141
+ #
142
+ # Unused. Kept for backward compatibility with older compilers.
176
143
fun __crystal_once_init : Void *
177
144
Pointer (Void ).null
178
145
end
0 commit comments