crystalruby 0.2.3 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +2 -0
- data/Dockerfile +23 -2
- data/README.md +395 -198
- data/Rakefile +4 -3
- data/crystalruby.gemspec +2 -2
- data/examples/adder/adder.rb +1 -1
- data/exe/crystalruby +1 -0
- data/lib/crystalruby/adapter.rb +143 -73
- data/lib/crystalruby/arc_mutex.rb +47 -0
- data/lib/crystalruby/compilation.rb +32 -3
- data/lib/crystalruby/config.rb +41 -37
- data/lib/crystalruby/function.rb +216 -73
- data/lib/crystalruby/library.rb +157 -51
- data/lib/crystalruby/reactor.rb +63 -44
- data/lib/crystalruby/source_reader.rb +92 -0
- data/lib/crystalruby/template.rb +16 -5
- data/lib/crystalruby/templates/function.cr +11 -10
- data/lib/crystalruby/templates/index.cr +53 -66
- data/lib/crystalruby/templates/inline_chunk.cr +1 -1
- data/lib/crystalruby/templates/ruby_interface.cr +34 -0
- data/lib/crystalruby/templates/top_level_function.cr +62 -0
- data/lib/crystalruby/templates/top_level_ruby_interface.cr +33 -0
- data/lib/crystalruby/typebuilder.rb +11 -55
- data/lib/crystalruby/typemaps.rb +92 -67
- data/lib/crystalruby/types/concerns/allocator.rb +80 -0
- data/lib/crystalruby/types/fixed_width/named_tuple.cr +80 -0
- data/lib/crystalruby/types/fixed_width/named_tuple.rb +86 -0
- data/lib/crystalruby/types/fixed_width/proc.cr +45 -0
- data/lib/crystalruby/types/fixed_width/proc.rb +79 -0
- data/lib/crystalruby/types/fixed_width/tagged_union.cr +53 -0
- data/lib/crystalruby/types/fixed_width/tagged_union.rb +113 -0
- data/lib/crystalruby/types/fixed_width/tuple.cr +82 -0
- data/lib/crystalruby/types/fixed_width/tuple.rb +92 -0
- data/lib/crystalruby/types/fixed_width.cr +138 -0
- data/lib/crystalruby/types/fixed_width.rb +205 -0
- data/lib/crystalruby/types/primitive.cr +21 -0
- data/lib/crystalruby/types/primitive.rb +117 -0
- data/lib/crystalruby/types/primitive_types/bool.cr +34 -0
- data/lib/crystalruby/types/primitive_types/bool.rb +11 -0
- data/lib/crystalruby/types/primitive_types/nil.cr +35 -0
- data/lib/crystalruby/types/primitive_types/nil.rb +16 -0
- data/lib/crystalruby/types/primitive_types/numbers.cr +37 -0
- data/lib/crystalruby/types/primitive_types/numbers.rb +28 -0
- data/lib/crystalruby/types/primitive_types/symbol.cr +55 -0
- data/lib/crystalruby/types/primitive_types/symbol.rb +35 -0
- data/lib/crystalruby/types/primitive_types/time.cr +35 -0
- data/lib/crystalruby/types/primitive_types/time.rb +25 -0
- data/lib/crystalruby/types/type.cr +64 -0
- data/lib/crystalruby/types/type.rb +249 -30
- data/lib/crystalruby/types/variable_width/array.cr +74 -0
- data/lib/crystalruby/types/variable_width/array.rb +88 -0
- data/lib/crystalruby/types/variable_width/hash.cr +146 -0
- data/lib/crystalruby/types/variable_width/hash.rb +117 -0
- data/lib/crystalruby/types/variable_width/string.cr +36 -0
- data/lib/crystalruby/types/variable_width/string.rb +18 -0
- data/lib/crystalruby/types/variable_width.cr +23 -0
- data/lib/crystalruby/types/variable_width.rb +46 -0
- data/lib/crystalruby/types.rb +32 -13
- data/lib/crystalruby/version.rb +2 -2
- data/lib/crystalruby.rb +13 -6
- metadata +42 -22
- data/lib/crystalruby/types/array.rb +0 -15
- data/lib/crystalruby/types/bool.rb +0 -3
- data/lib/crystalruby/types/hash.rb +0 -17
- data/lib/crystalruby/types/named_tuple.rb +0 -28
- data/lib/crystalruby/types/nil.rb +0 -3
- data/lib/crystalruby/types/numbers.rb +0 -5
- data/lib/crystalruby/types/string.rb +0 -3
- data/lib/crystalruby/types/symbol.rb +0 -3
- data/lib/crystalruby/types/time.rb +0 -8
- data/lib/crystalruby/types/tuple.rb +0 -17
- data/lib/crystalruby/types/type_serializer/json.rb +0 -41
- data/lib/crystalruby/types/type_serializer.rb +0 -37
- data/lib/crystalruby/types/typedef.rb +0 -57
- data/lib/crystalruby/types/union_type.rb +0 -43
- data/lib/module.rb +0 -3
data/lib/crystalruby/library.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module CrystalRuby
|
2
4
|
class Library
|
3
5
|
include Typemaps
|
@@ -8,7 +10,8 @@ module CrystalRuby
|
|
8
10
|
CR_COMPILE_MUX = Mutex.new
|
9
11
|
CR_ATTACH_MUX = Mutex.new
|
10
12
|
|
11
|
-
attr_accessor :name, :methods, :
|
13
|
+
attr_accessor :name, :methods, :exposed_methods, :chunks, :root_dir,
|
14
|
+
:lib_dir, :src_dir, :codegen_dir, :shards
|
12
15
|
|
13
16
|
@libs_by_name = {}
|
14
17
|
|
@@ -29,7 +32,9 @@ module CrystalRuby
|
|
29
32
|
def initialize(name)
|
30
33
|
self.name = name
|
31
34
|
self.methods = {}
|
35
|
+
self.exposed_methods = {}
|
32
36
|
self.chunks = []
|
37
|
+
self.shards = {}
|
33
38
|
initialize_library!
|
34
39
|
end
|
35
40
|
|
@@ -44,7 +49,7 @@ module CrystalRuby
|
|
44
49
|
].each do |dir|
|
45
50
|
FileUtils.mkdir_p(dir)
|
46
51
|
end
|
47
|
-
IO.write
|
52
|
+
IO.write main_file, "require \"./#{config.crystal_codegen_dir}/index\"\n" unless File.exist?(main_file)
|
48
53
|
|
49
54
|
return if File.exist?(shard_file)
|
50
55
|
|
@@ -56,7 +61,7 @@ module CrystalRuby
|
|
56
61
|
|
57
62
|
# Generates and stores a reference to a new CrystalRuby::Function
|
58
63
|
# and triggers the generation of the crystal code. (See write_chunk)
|
59
|
-
def
|
64
|
+
def crystallize_method(method, args, returns, function_body, async, &block)
|
60
65
|
CR_ATTACH_MUX.synchronize do
|
61
66
|
methods.each_value(&:unattach!)
|
62
67
|
method_key = "#{method.owner.name}/#{method.name}"
|
@@ -69,9 +74,26 @@ module CrystalRuby
|
|
69
74
|
lib: self,
|
70
75
|
&block
|
71
76
|
).tap do |func|
|
72
|
-
func.
|
77
|
+
func.define_crystallized_methods!(self)
|
73
78
|
func.register_custom_types!(self)
|
74
|
-
write_chunk(
|
79
|
+
write_chunk(func.owner_name, method.name, func.chunk)
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
def expose_method(method, args, returns)
|
85
|
+
CR_ATTACH_MUX.synchronize do
|
86
|
+
methods.each_value(&:unattach!)
|
87
|
+
method_key = "#{method.owner.name}/#{method.name}"
|
88
|
+
methods[method_key] = Function.new(
|
89
|
+
method: method,
|
90
|
+
args: args,
|
91
|
+
returns: returns,
|
92
|
+
ruby: true,
|
93
|
+
lib: self
|
94
|
+
).tap do |func|
|
95
|
+
func.register_custom_types!(self)
|
96
|
+
write_chunk(func.owner_name, method.name, func.ruby_interface)
|
75
97
|
end
|
76
98
|
end
|
77
99
|
end
|
@@ -88,8 +110,8 @@ module CrystalRuby
|
|
88
110
|
src_dir / "shard.yml"
|
89
111
|
end
|
90
112
|
|
91
|
-
def
|
92
|
-
write_chunk(mod.name, chunk_name, body)
|
113
|
+
def crystallize_chunk(mod, chunk_name, body)
|
114
|
+
write_chunk(mod.respond_to?(:name) ? name : "main", chunk_name, body)
|
93
115
|
end
|
94
116
|
|
95
117
|
def instantiated?
|
@@ -102,7 +124,15 @@ module CrystalRuby
|
|
102
124
|
file_digest = Digest::MD5.hexdigest chunk_data
|
103
125
|
fname = chunk[:chunk_name]
|
104
126
|
index_contents.include?("#{chunk[:module_name]}/#{fname}_#{file_digest}.cr")
|
105
|
-
end
|
127
|
+
end && shards_installed?
|
128
|
+
end
|
129
|
+
|
130
|
+
def shards_installed?
|
131
|
+
shard_file_content = nil
|
132
|
+
shards.all? do |k, v|
|
133
|
+
dependencies ||= shard_file_contents["dependencies"]
|
134
|
+
dependencies[k] == v
|
135
|
+
end && CrystalRuby::Compilation.shard_check?(src_dir)
|
106
136
|
end
|
107
137
|
|
108
138
|
def index_contents
|
@@ -112,50 +142,126 @@ module CrystalRuby
|
|
112
142
|
end
|
113
143
|
|
114
144
|
def register_type!(type)
|
115
|
-
|
116
|
-
@types_cache[type.name] = type.type_defn
|
145
|
+
write_chunk("types", type.crystal_class_name, build_type(type.crystal_class_name, type.type_defn))
|
117
146
|
end
|
118
147
|
|
119
|
-
def
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
148
|
+
def build_type(type_name, expr)
|
149
|
+
parts = type_name.split("::")
|
150
|
+
typedef = parts[0...-1].each_with_index.reduce("") do |acc, (part, index)|
|
151
|
+
acc + "#{" " * index}module #{part}\n"
|
152
|
+
end
|
153
|
+
typedef += "#{" " * parts.size}#{expr}\n"
|
154
|
+
typedef + parts[0...-1].reverse.each_with_index.reduce("") do |acc, (_part, index)|
|
155
|
+
acc + "#{" " * (parts.size - 2 - index)}end\n"
|
156
|
+
end
|
157
|
+
end
|
158
|
+
|
159
|
+
def shard_file_contents
|
160
|
+
@shard_file_contents ||= YAML.safe_load(IO.read(shard_file))
|
161
|
+
rescue StandardError
|
162
|
+
@shard_file_contents ||= { "name" => "src", "version" => "0.1.0", "dependencies" => {} }
|
163
|
+
end
|
164
|
+
|
165
|
+
def shard_file_contents=(contents)
|
166
|
+
IO.write(shard_file, JSON.load(contents.to_json).to_yaml)
|
167
|
+
end
|
168
|
+
|
169
|
+
def shard_dependencies
|
170
|
+
shard_file_contents["dependencies"] ||= {}
|
171
|
+
end
|
172
|
+
|
173
|
+
def require_shard(name, opts)
|
174
|
+
@shards[name.to_s] = JSON.parse(opts.merge("_crystalruby_managed" => true).to_json)
|
175
|
+
rewrite_shards_file!
|
176
|
+
end
|
177
|
+
|
178
|
+
def rewrite_shards_file!
|
179
|
+
dependencies = shard_dependencies
|
180
|
+
|
181
|
+
dirty = @shards.any? do |k, v|
|
182
|
+
dependencies[k] != v
|
183
|
+
end || (@shards.empty? && dependencies.any?)
|
184
|
+
|
185
|
+
return unless dirty
|
186
|
+
|
187
|
+
if @shards.empty?
|
188
|
+
shard_file_contents.delete("dependencies")
|
189
|
+
else
|
190
|
+
shard_file_contents["dependencies"] = @shards
|
191
|
+
end
|
192
|
+
|
193
|
+
self.shard_file_contents = shard_file_contents
|
130
194
|
end
|
131
195
|
|
132
196
|
def requires
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
197
|
+
Template::Type.render({}) +
|
198
|
+
Template::Primitive.render({}) +
|
199
|
+
Template::FixedWidth.render({}) +
|
200
|
+
Template::VariableWidth.render({}) +
|
201
|
+
chunks.map do |chunk|
|
202
|
+
chunk_data = chunk[:body]
|
203
|
+
file_digest = Digest::MD5.hexdigest chunk_data
|
204
|
+
fname = chunk[:chunk_name]
|
205
|
+
"require \"./#{chunk[:module_name]}/#{fname}_#{file_digest}.cr\"\n"
|
206
|
+
end.join("\n") + shards.keys.map do |shard_name|
|
207
|
+
"require \"#{shard_name}\"\n"
|
208
|
+
end.join("\n")
|
139
209
|
end
|
140
210
|
|
141
211
|
def build!
|
142
212
|
CR_COMPILE_MUX.synchronize do
|
143
|
-
File.write codegen_dir / "index.cr", Template::Index.render(
|
144
|
-
type_modules: type_modules,
|
145
|
-
requires: requires
|
146
|
-
)
|
147
|
-
|
213
|
+
File.write codegen_dir / "index.cr", Template::Index.render(requires: requires)
|
148
214
|
unless compiled?
|
215
|
+
FileUtils.rm_f(lib_file)
|
216
|
+
|
217
|
+
if shard_dependencies.any? && shards.empty?
|
218
|
+
rewrite_shards_file!
|
219
|
+
end
|
220
|
+
|
221
|
+
CrystalRuby::Compilation.install_shards!(src_dir)
|
149
222
|
CrystalRuby::Compilation.compile!(
|
150
223
|
verbose: config.verbose,
|
151
224
|
debug: config.debug,
|
152
225
|
src: main_file,
|
153
|
-
lib: lib_file
|
226
|
+
lib: "#{lib_file}.part"
|
154
227
|
)
|
228
|
+
FileUtils.mv("#{lib_file}.part", lib_file)
|
229
|
+
attach!
|
155
230
|
end
|
156
231
|
end
|
157
232
|
end
|
158
233
|
|
234
|
+
def attach!
|
235
|
+
CR_ATTACH_MUX.synchronize do
|
236
|
+
lib_file = self.lib_file
|
237
|
+
lib_methods = methods
|
238
|
+
lib_methods.values.reject(&:ruby).each(&:attach_ffi_func!)
|
239
|
+
singleton_class.class_eval do
|
240
|
+
extend FFI::Library
|
241
|
+
ffi_lib lib_file
|
242
|
+
%i[yield init].each do |method_name|
|
243
|
+
singleton_class.undef_method(method_name) if singleton_class.method_defined?(method_name)
|
244
|
+
undef_method(method_name) if method_defined?(method_name)
|
245
|
+
end
|
246
|
+
attach_function :init, %i[string pointer pointer], :void
|
247
|
+
attach_function :yield, %i[], :int
|
248
|
+
lib_methods.each_value.select(&:ruby).each do |method|
|
249
|
+
attach_function :"register_#{method.name.to_s.gsub("?", "q").gsub("=", "eq").gsub("!", "bang")}_callback", %i[pointer], :void
|
250
|
+
end
|
251
|
+
end
|
252
|
+
|
253
|
+
if CrystalRuby.config.single_thread_mode
|
254
|
+
Reactor.init_single_thread_mode!
|
255
|
+
else
|
256
|
+
Reactor.start!
|
257
|
+
end
|
258
|
+
|
259
|
+
Reactor.schedule_work!(self, :init, name, Reactor::ERROR_CALLBACK, Types::Type::ARC_MUTEX.to_ptr, :void,
|
260
|
+
blocking: true, async: false)
|
261
|
+
methods.values.select(&:ruby).each(&:register_callback!)
|
262
|
+
end
|
263
|
+
end
|
264
|
+
|
159
265
|
def digest
|
160
266
|
Digest::MD5.hexdigest(File.read(codegen_dir / "index.cr")) if File.exist?(codegen_dir / "index.cr")
|
161
267
|
end
|
@@ -165,31 +271,31 @@ module CrystalRuby
|
|
165
271
|
end
|
166
272
|
|
167
273
|
def write_chunk(module_name, chunk_name, body)
|
274
|
+
module_name = module_name.gsub("::", "__MOD__")
|
168
275
|
chunks.delete_if { |chnk| chnk[:module_name] == module_name && chnk[:chunk_name] == chunk_name }
|
169
|
-
|
276
|
+
chunk = { module_name: module_name, chunk_name: chunk_name, body: body }
|
277
|
+
chunks << chunk
|
170
278
|
existing = Dir.glob(codegen_dir / "**/*.cr")
|
171
279
|
|
172
280
|
current_index_contents = index_contents
|
173
|
-
|
174
|
-
module_name, chunk_name, body = chunk.values_at(:module_name, :chunk_name, :body)
|
281
|
+
module_name, chunk_name, body = chunk.values_at(:module_name, :chunk_name, :body)
|
175
282
|
|
176
|
-
|
177
|
-
|
283
|
+
file_digest = Digest::MD5.hexdigest body
|
284
|
+
filename = (codegen_dir / module_name / "#{chunk_name}_#{file_digest}.cr").to_s
|
178
285
|
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
|
286
|
+
unless current_index_contents.include?("#{module_name}/#{chunk_name}_#{file_digest}.cr")
|
287
|
+
methods.each_value(&:unattach!)
|
288
|
+
@compiled = false
|
289
|
+
end
|
183
290
|
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
|
188
|
-
|
189
|
-
|
190
|
-
|
191
|
-
|
192
|
-
end
|
291
|
+
unless existing.delete(filename)
|
292
|
+
FileUtils.mkdir_p(codegen_dir / module_name)
|
293
|
+
File.write(filename, body)
|
294
|
+
end
|
295
|
+
existing.select do |f|
|
296
|
+
f =~ /#{config.crystal_codegen_dir / module_name / "#{chunk_name}_[a-f0-9]{32}\.cr"}/
|
297
|
+
end.each do |fl|
|
298
|
+
File.delete(fl) unless fl.eql?(filename)
|
193
299
|
end
|
194
300
|
end
|
195
301
|
end
|
data/lib/crystalruby/reactor.rb
CHANGED
@@ -1,13 +1,17 @@
|
|
1
1
|
module CrystalRuby
|
2
|
+
require 'json'
|
2
3
|
# The Reactor represents a singleton Thread responsible for running all Ruby/crystal interop code.
|
3
4
|
# Crystal's Fiber scheduler and GC assume all code is run on a single thread.
|
4
|
-
# This class is responsible for multiplexing Ruby and Crystal code
|
5
|
-
# Functions annotated with async: true, are executed using callbacks to allow these to be interleaved
|
6
|
-
|
5
|
+
# This class is responsible for multiplexing Ruby and Crystal code onto a single thread.
|
6
|
+
# Functions annotated with async: true, are executed using callbacks to allow these to be interleaved
|
7
|
+
# without blocking multiple Ruby threads.
|
7
8
|
module Reactor
|
8
9
|
module_function
|
9
10
|
|
10
11
|
class SingleThreadViolation < StandardError; end
|
12
|
+
class StopReactor < StandardError; end
|
13
|
+
|
14
|
+
@single_thread_mode = false
|
11
15
|
|
12
16
|
REACTOR_QUEUE = Queue.new
|
13
17
|
|
@@ -39,13 +43,15 @@ module CrystalRuby
|
|
39
43
|
end
|
40
44
|
end
|
41
45
|
|
42
|
-
ERROR_CALLBACK = FFI::Function.new(:void, %i[string string int]) do |error_type, message, tid|
|
46
|
+
ERROR_CALLBACK = FFI::Function.new(:void, %i[string string string int]) do |error_type, message, backtrace, tid|
|
43
47
|
error_type = error_type.to_sym
|
44
48
|
is_exception_type = Object.const_defined?(error_type) && Object.const_get(error_type).ancestors.include?(Exception)
|
45
49
|
error_type = is_exception_type ? Object.const_get(error_type) : RuntimeError
|
46
|
-
|
50
|
+
error = error_type.new(message)
|
51
|
+
error.set_backtrace(JSON.parse(backtrace))
|
52
|
+
raise error unless THREAD_MAP.key?(tid)
|
47
53
|
|
48
|
-
THREAD_MAP[tid][:error] =
|
54
|
+
THREAD_MAP[tid][:error] = error
|
49
55
|
THREAD_MAP[tid][:result] = nil
|
50
56
|
THREAD_MAP[tid][:cond].signal
|
51
57
|
end
|
@@ -55,20 +61,41 @@ module CrystalRuby
|
|
55
61
|
end
|
56
62
|
|
57
63
|
def await_result!
|
58
|
-
mux, cond = thread_conditions.values_at(:mux, :cond)
|
59
|
-
cond.wait(mux)
|
60
|
-
|
64
|
+
mux, cond, result, err = thread_conditions.values_at(:mux, :cond, :result, :error)
|
65
|
+
cond.wait(mux) unless (result || err)
|
66
|
+
result, err, thread_conditions[:result], thread_conditions[:error] = thread_conditions.values_at(:result, :error)
|
67
|
+
if err
|
68
|
+
combined_backtrace = err.backtrace[0..(err.backtrace.index{|m| m.include?('call_blocking_function')} || 2) - 3] + caller[5..-1]
|
69
|
+
err.set_backtrace(combined_backtrace)
|
70
|
+
raise err
|
71
|
+
end
|
72
|
+
|
73
|
+
result
|
74
|
+
end
|
75
|
+
|
76
|
+
def halt_loop!
|
77
|
+
raise StopReactor
|
78
|
+
end
|
61
79
|
|
62
|
-
|
80
|
+
def stop!
|
81
|
+
if @main_loop
|
82
|
+
schedule_work!(self, :halt_loop!, :void, blocking: true, async: false)
|
83
|
+
@main_loop.join
|
84
|
+
@main_loop = nil
|
85
|
+
CrystalRuby.log_info "Reactor loop stopped"
|
86
|
+
end
|
63
87
|
end
|
64
88
|
|
65
89
|
def start!
|
66
90
|
@main_loop ||= Thread.new do
|
91
|
+
@main_thread_id = Thread.current.object_id
|
67
92
|
CrystalRuby.log_debug("Starting reactor")
|
68
93
|
CrystalRuby.log_debug("CrystalRuby initialized")
|
69
|
-
|
70
|
-
REACTOR_QUEUE.pop
|
94
|
+
while true
|
95
|
+
handler, *args = REACTOR_QUEUE.pop
|
96
|
+
send(handler, *args)
|
71
97
|
end
|
98
|
+
rescue StopReactor => e
|
72
99
|
rescue StandardError => e
|
73
100
|
CrystalRuby.log_error "Error: #{e}"
|
74
101
|
CrystalRuby.log_error e.backtrace
|
@@ -80,26 +107,40 @@ module CrystalRuby
|
|
80
107
|
end
|
81
108
|
|
82
109
|
def yield!(lib: nil, time: 0.0)
|
83
|
-
schedule_work!(lib, :yield,
|
110
|
+
schedule_work!(lib, :yield, :int, async: false, blocking: false, lib: lib) if running? && lib
|
84
111
|
nil
|
85
112
|
end
|
86
113
|
|
87
|
-
def
|
88
|
-
|
114
|
+
def invoke_async!(receiver, op_name, *args, thread_id, callback, lib)
|
115
|
+
receiver.send(op_name, *args, thread_id, callback)
|
116
|
+
yield!(lib: lib, time: 0)
|
89
117
|
end
|
90
118
|
|
91
|
-
def
|
92
|
-
|
119
|
+
def invoke_blocking!(receiver, op_name, *args, tvars)
|
120
|
+
tvars[:error] = nil
|
121
|
+
begin
|
122
|
+
tvars[:result] = receiver.send(op_name, *args)
|
123
|
+
rescue StopReactor => e
|
124
|
+
tvars[:cond].signal
|
125
|
+
raise
|
126
|
+
rescue StandardError => e
|
127
|
+
tvars[:error] = e
|
128
|
+
end
|
129
|
+
tvars[:cond].signal
|
130
|
+
end
|
131
|
+
|
132
|
+
def invoke_await!(receiver, op_name, *args, lib)
|
133
|
+
outstanding_jobs = receiver.send(op_name, *args)
|
134
|
+
yield!(lib: lib, time: 0) unless outstanding_jobs == 0
|
93
135
|
end
|
94
136
|
|
95
137
|
def schedule_work!(receiver, op_name, *args, return_type, blocking: true, async: true, lib: nil)
|
96
|
-
if @single_thread_mode
|
138
|
+
if @single_thread_mode || (Thread.current.object_id == @main_thread_id && op_name != :yield)
|
97
139
|
unless Thread.current.object_id == @main_thread_id
|
98
140
|
raise SingleThreadViolation,
|
99
141
|
"Single thread mode is enabled, cannot run in multi-threaded mode. " \
|
100
142
|
"Reactor was started from: #{@main_thread_id}, then called from #{Thread.current.object_id}"
|
101
143
|
end
|
102
|
-
|
103
144
|
return receiver.send(op_name, *args)
|
104
145
|
end
|
105
146
|
|
@@ -107,31 +148,9 @@ module CrystalRuby
|
|
107
148
|
tvars[:mux].synchronize do
|
108
149
|
REACTOR_QUEUE.push(
|
109
150
|
case true
|
110
|
-
when async
|
111
|
-
|
112
|
-
|
113
|
-
op_name, *args, tvars[:thread_id],
|
114
|
-
CALLBACKS_MAP[return_type]
|
115
|
-
)
|
116
|
-
yield!(lib: lib, time: 0)
|
117
|
-
}
|
118
|
-
when blocking
|
119
|
-
lambda {
|
120
|
-
tvars[:error] = nil
|
121
|
-
Reactor.current_thread_id = tvars[:thread_id]
|
122
|
-
begin
|
123
|
-
result = receiver.send(op_name, *args)
|
124
|
-
rescue StandardError => e
|
125
|
-
tvars[:error] = e
|
126
|
-
end
|
127
|
-
tvars[:result] = result unless tvars[:error]
|
128
|
-
tvars[:cond].signal
|
129
|
-
}
|
130
|
-
else
|
131
|
-
lambda {
|
132
|
-
outstanding_jobs = receiver.send(op_name, *args)
|
133
|
-
yield!(lib: lib, time: 0) unless outstanding_jobs == 0
|
134
|
-
}
|
151
|
+
when async then [:invoke_async!, receiver, op_name, *args, tvars[:thread_id], CALLBACKS_MAP[return_type], lib]
|
152
|
+
when blocking then [:invoke_blocking!, receiver, op_name, *args, tvars]
|
153
|
+
else [:invoke_await!, receiver, op_name, *args, lib]
|
135
154
|
end
|
136
155
|
)
|
137
156
|
return await_result! if blocking
|
@@ -0,0 +1,92 @@
|
|
1
|
+
module CrystalRuby
|
2
|
+
module SourceReader
|
3
|
+
module_function
|
4
|
+
|
5
|
+
# Reads code line by line from a given source location and returns the first valid Ruby expression found
|
6
|
+
def extract_expr_from_source_location(source_location)
|
7
|
+
lines = source_location.then{|f,l| IO.readlines(f)[l-1..]}
|
8
|
+
lines[0] = lines[0][/CRType.*/] if lines[0] =~ /<\s+CRType/ || lines[0] =~ /= CRType/
|
9
|
+
lines.each.with_object([]) do |line, expr_source|
|
10
|
+
break expr_source.join("") if (Prism.parse((expr_source << line).join("")).success?)
|
11
|
+
end
|
12
|
+
rescue
|
13
|
+
raise "Failed to extract expression from source location: #{source_location}. Ensure the file exists and the line number is correct. Extraction from a REPL is not supported"
|
14
|
+
end
|
15
|
+
|
16
|
+
def search_node(result, node_type)
|
17
|
+
result.breadth_first_search do |node|
|
18
|
+
node_type === node
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
# Given a proc, extracts the source code of the block passed to it
|
23
|
+
# If raw is true, the source is expected to be Raw Crystal code captured
|
24
|
+
# in a string or Heredoc literal. Otherwise the Ruby code (assumed to be valid Crystal)
|
25
|
+
# is extracted.
|
26
|
+
def extract_source_from_proc(block, raw: false)
|
27
|
+
block_source = extract_expr_from_source_location(block.source_location)
|
28
|
+
parsed_source = Prism.parse(block_source).value
|
29
|
+
|
30
|
+
node = parsed_source.statements.body[0].arguments&.arguments&.find{|x| search_node(x, Prism::StatementsNode) }
|
31
|
+
node ||= parsed_source.statements.body[0]
|
32
|
+
body_node = search_node(node, Prism::StatementsNode)
|
33
|
+
|
34
|
+
return raw ?
|
35
|
+
extract_raw_string_node(body_node) :
|
36
|
+
node_to_s(body_node)
|
37
|
+
end
|
38
|
+
|
39
|
+
def extract_raw_string_node(node)
|
40
|
+
search_node(node, Prism::InterpolatedStringNode)&.parts&.map(&:unescaped)&.join("") ||
|
41
|
+
search_node(node, Prism::StringNode).unescaped
|
42
|
+
end
|
43
|
+
|
44
|
+
|
45
|
+
# Simple helper function to turn a SyntaxTree node back into a Ruby string
|
46
|
+
# The default formatter will turn a break/return of [1,2,3] into a brackless 1,2,3
|
47
|
+
# Can't have that in Crystal as it turns it into a Tuple
|
48
|
+
def node_to_s(node)
|
49
|
+
node&.slice || ''
|
50
|
+
end
|
51
|
+
|
52
|
+
# Given a method, extracts the source code of the block passed to it
|
53
|
+
# and also converts any keyword arguments given in the method definition as a
|
54
|
+
# named map of keyword names to Crystal types.
|
55
|
+
# Also supports basic ffi symbol types.
|
56
|
+
#
|
57
|
+
# E.g.
|
58
|
+
#
|
59
|
+
# def add a: Int32 | Int64, b: :int
|
60
|
+
#
|
61
|
+
# The above will be converted to:
|
62
|
+
# {
|
63
|
+
# a: Int32 | Int64, # Int32 | Int64 is a Crystal type
|
64
|
+
# b: :int # :int is an FFI type shorthand
|
65
|
+
# }
|
66
|
+
# If raw is true, the source is expected to be Raw Crystal code captured
|
67
|
+
# in a string or Heredoc literal. Otherwise the Ruby code (assumed to be valid Crystal)
|
68
|
+
# is extracted.
|
69
|
+
def extract_args_and_source_from_method(method, raw: false)
|
70
|
+
method_source = extract_expr_from_source_location(method.source_location)
|
71
|
+
parsed_source = Prism.parse(method_source).value
|
72
|
+
params = search_node(parsed_source, Prism::ParametersNode)
|
73
|
+
args = params ? params.keywords.map{|kw| [kw.name, node_to_s(kw.value)] }.to_h : {}
|
74
|
+
body_node = parsed_source.statements.body[0].body
|
75
|
+
if body_node.respond_to?(:rescue_clause) && body_node.rescue_clause
|
76
|
+
wrapped = %{begin\n#{body_node.statements.slice}\n#{body_node.rescue_clause.slice}\nend}
|
77
|
+
body_node = Prism.parse(wrapped).value
|
78
|
+
end
|
79
|
+
body = raw ? extract_raw_string_node(body_node) : node_to_s(body_node)
|
80
|
+
|
81
|
+
args.transform_values! do |type_exp|
|
82
|
+
if CrystalRuby::Typemaps::CRYSTAL_TYPE_MAP.key?(type_exp[1..-1].to_sym)
|
83
|
+
type_exp[1..-1].to_sym
|
84
|
+
else
|
85
|
+
TypeBuilder.build_from_source(type_exp, context: method.owner)
|
86
|
+
end
|
87
|
+
end.to_h
|
88
|
+
return args, body
|
89
|
+
end
|
90
|
+
|
91
|
+
end
|
92
|
+
end
|
data/lib/crystalruby/template.rb
CHANGED
@@ -1,12 +1,23 @@
|
|
1
1
|
module CrystalRuby
|
2
2
|
module Template
|
3
|
-
|
3
|
+
class Renderer < Struct.new(:raw_value)
|
4
|
+
require 'erb'
|
5
|
+
def render(context)
|
6
|
+
if context.kind_of?(::Hash)
|
7
|
+
raw_value % context
|
8
|
+
else
|
9
|
+
ERB.new(raw_value, trim_mode: "%").result(context)
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
(
|
15
|
+
Dir[File.join(File.dirname(__FILE__), "templates", "**", "*.cr")] +
|
16
|
+
Dir[File.join(File.dirname(__FILE__), "types", "**", "*.cr")]
|
17
|
+
).each do |file|
|
4
18
|
template_name = File.basename(file, File.extname(file)).split("_").map(&:capitalize).join
|
5
19
|
template_value = File.read(file)
|
6
|
-
|
7
|
-
self % context
|
8
|
-
end
|
9
|
-
const_set(template_name, template_value)
|
20
|
+
const_set(template_name, Renderer.new(template_value))
|
10
21
|
end
|
11
22
|
end
|
12
23
|
end
|
@@ -3,8 +3,8 @@
|
|
3
3
|
# Crystal code can simply call this method directly, enabling generated crystal code
|
4
4
|
# to call other generated crystal code without overhead.
|
5
5
|
|
6
|
-
|
7
|
-
def
|
6
|
+
%{module_or_class} %{module_name} %{superclass}
|
7
|
+
def %{fn_scope}%{fn_name}(%{fn_args}) : %{fn_ret_type}
|
8
8
|
%{fn_body}
|
9
9
|
end
|
10
10
|
end
|
@@ -16,13 +16,13 @@ fun %{lib_fn_name}(%{lib_fn_args}): %{lib_fn_ret_type}
|
|
16
16
|
begin
|
17
17
|
%{convert_lib_args}
|
18
18
|
begin
|
19
|
-
return_value = %{
|
19
|
+
return_value = %{receiver}.%{fn_name}(%{arg_names})%{block_converter}
|
20
20
|
return %{convert_return_type}
|
21
21
|
rescue ex
|
22
|
-
CrystalRuby.report_error("RuntimeError", ex.message.to_s, 0)
|
22
|
+
CrystalRuby.report_error("RuntimeError", ex.message.to_s, ex.backtrace.to_json, 0)
|
23
23
|
end
|
24
24
|
rescue ex
|
25
|
-
CrystalRuby.report_error("ArgumentError", ex.message.to_s, 0)
|
25
|
+
CrystalRuby.report_error("ArgumentError", ex.message.to_s, ex.backtrace.to_json, 0)
|
26
26
|
end
|
27
27
|
return %{error_value}
|
28
28
|
end
|
@@ -37,25 +37,26 @@ fun %{lib_fn_name}_async(%{lib_fn_args} thread_id: UInt32, callback : %{callbac
|
|
37
37
|
CrystalRuby.increment_task_counter
|
38
38
|
spawn do
|
39
39
|
begin
|
40
|
-
return_value = %{
|
41
|
-
converted = %{convert_return_type}
|
40
|
+
return_value = %{receiver}.%{fn_name}(%{arg_names})%{block_converter}
|
42
41
|
CrystalRuby.queue_callback(->{
|
42
|
+
converted = %{convert_return_type}
|
43
43
|
%{callback_call}
|
44
44
|
CrystalRuby.decrement_task_counter
|
45
45
|
})
|
46
46
|
rescue ex
|
47
47
|
exception = ex.message.to_s
|
48
|
+
backtrace = ex.backtrace.to_json
|
48
49
|
CrystalRuby.queue_callback(->{
|
49
|
-
CrystalRuby.
|
50
|
+
CrystalRuby.report_error("RuntimeError", exception, backtrace, thread_id)
|
50
51
|
CrystalRuby.decrement_task_counter
|
51
52
|
})
|
52
53
|
end
|
53
54
|
end
|
54
55
|
rescue ex
|
55
|
-
|
56
56
|
exception = ex.message.to_s
|
57
|
+
backtrace = ex.backtrace.to_json
|
57
58
|
CrystalRuby.queue_callback(->{
|
58
|
-
CrystalRuby.
|
59
|
+
CrystalRuby.report_error("RuntimeError", ex.message.to_s, backtrace, thread_id)
|
59
60
|
CrystalRuby.decrement_task_counter
|
60
61
|
})
|
61
62
|
end
|