Skip to content

Commit cb536e9

Browse files
committed
Revert once flag to be a bool so we keep a single implementation
This avoids having two implementations and it will simplify the call site when we protect class getter/property macros with lazy initializers: the flag is always a Bool not either a Crystal::Once::State or a Bool depending on the compiler.
1 parent bd0bf02 commit cb536e9

File tree

4 files changed

+69
-105
lines changed

4 files changed

+69
-105
lines changed

src/compiler/crystal/codegen/class_var.cr

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,8 @@ class Crystal::CodeGenVisitor
2525
initialized_flag_name = class_var_global_initialized_name(class_var)
2626
initialized_flag = @main_mod.globals[initialized_flag_name]?
2727
unless initialized_flag
28-
initialized_flag = @main_mod.globals.add(@main_llvm_context.int8, initialized_flag_name)
29-
initialized_flag.initializer = @main_llvm_context.int8.const_int(0)
28+
initialized_flag = @main_mod.globals.add(@main_llvm_context.int1, initialized_flag_name)
29+
initialized_flag.initializer = @main_llvm_context.int1.const_int(0)
3030
initialized_flag.linkage = LLVM::Linkage::Internal if @single_module
3131
initialized_flag.thread_local = true if class_var.thread_local?
3232
end
@@ -61,7 +61,7 @@ class Crystal::CodeGenVisitor
6161
initialized_flag_name = class_var_global_initialized_name(class_var)
6262
initialized_flag = @llvm_mod.globals[initialized_flag_name]?
6363
unless initialized_flag
64-
initialized_flag = @llvm_mod.globals.add(llvm_context.int8, initialized_flag_name)
64+
initialized_flag = @llvm_mod.globals.add(llvm_context.int1, initialized_flag_name)
6565
initialized_flag.thread_local = true if class_var.thread_local?
6666
end
6767
end

src/compiler/crystal/codegen/const.cr

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -64,8 +64,8 @@ class Crystal::CodeGenVisitor
6464
initialized_flag_name = const.initialized_llvm_name
6565
initialized_flag = @main_mod.globals[initialized_flag_name]?
6666
unless initialized_flag
67-
initialized_flag = @main_mod.globals.add(@main_llvm_context.int8, initialized_flag_name)
68-
initialized_flag.initializer = @main_llvm_context.int8.const_int(0)
67+
initialized_flag = @main_mod.globals.add(@main_llvm_context.int1, initialized_flag_name)
68+
initialized_flag.initializer = @main_llvm_context.int1.const_int(0)
6969
initialized_flag.linkage = LLVM::Linkage::Internal if @single_module
7070
end
7171
initialized_flag

src/compiler/crystal/codegen/once.cr

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -40,9 +40,6 @@ class Crystal::CodeGenVisitor
4040
end
4141

4242
state = load(once_state_type, once_state_global)
43-
{% if LibLLVM::IS_LT_150 %}
44-
flag = bit_cast(flag, @llvm_context.int1.pointer) # cast Int8* to Bool*
45-
{% end %}
4643
args = [state, flag, initializer]
4744
end
4845

src/crystal/once.cr

Lines changed: 64 additions & 97 deletions
Original file line numberDiff line numberDiff line change
@@ -14,25 +14,13 @@ require "crystal/spin_lock"
1414
module Crystal
1515
# :nodoc:
1616
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-
2917
struct Operation
3018
include PointerLinkedList::Node
3119

3220
getter fiber : Fiber
33-
getter flag : FlagT*
21+
getter flag : Bool*
3422

35-
def initialize(@flag : FlagT*, @fiber : Fiber)
23+
def initialize(@flag : Bool*, @fiber : Fiber)
3624
@waiting = PointerLinkedList(Fiber::PointerLinkedListNode).new
3725
end
3826

@@ -53,126 +41,105 @@ module Crystal
5341
@@operations = PointerLinkedList(Operation).new
5442
end
5543

56-
protected def self.exec(flag : FlagT*, &)
44+
protected def self.exec(flag : Bool*, &)
5745
@@spin.lock
5846

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
6055

6156
# safety check, and allows to safely call `Intrinsics.unreachable` in
6257
# `__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
6859

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"
7061
LibC._exit(1)
7162
end
7263

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
7667
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, &)
7785
operation = Operation.new(flag, Fiber.current)
7886
@@operations.push pointerof(operation)
7987
@@spin.unlock
8088

8189
yield
8290

8391
@@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
8993
@@operations.delete pointerof(operation)
9094
@@spin.unlock
9195

9296
operation.resume_all
9397
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
11998
end
12099

121100
# :nodoc:
101+
#
102+
# Never inlined to avoid bloating the call site with the slow-path that should
103+
# usually not be taken.
122104
@[NoInline]
123-
def self.once(flag : Once::FlagT*, initializer : Void*)
105+
def self.once(flag : Bool*, initializer : Void*)
124106
Once.exec(flag, &Proc(Nil).new(initializer, Pointer(Void).null))
125107
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
143108

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
148121
end
122+
end
149123

124+
{% if compare_versions(Crystal::VERSION, "1.16.0-dev") >= 0 %}
150125
# :nodoc:
126+
#
127+
# We always inline this accessor to optimize for the fast-path (already
128+
# initialized).
151129
@[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
154132
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
168133

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
173138
end
174-
139+
{% else %}
175140
# :nodoc:
141+
#
142+
# Unused. Kept for backward compatibility with older compilers.
176143
fun __crystal_once_init : Void*
177144
Pointer(Void).null
178145
end

0 commit comments

Comments
 (0)