crystalruby 0.2.3 → 0.3.1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|