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.
Files changed (77) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +2 -0
  3. data/Dockerfile +23 -2
  4. data/README.md +395 -198
  5. data/Rakefile +4 -3
  6. data/crystalruby.gemspec +2 -2
  7. data/examples/adder/adder.rb +1 -1
  8. data/exe/crystalruby +1 -0
  9. data/lib/crystalruby/adapter.rb +143 -73
  10. data/lib/crystalruby/arc_mutex.rb +47 -0
  11. data/lib/crystalruby/compilation.rb +32 -3
  12. data/lib/crystalruby/config.rb +41 -37
  13. data/lib/crystalruby/function.rb +216 -73
  14. data/lib/crystalruby/library.rb +157 -51
  15. data/lib/crystalruby/reactor.rb +63 -44
  16. data/lib/crystalruby/source_reader.rb +92 -0
  17. data/lib/crystalruby/template.rb +16 -5
  18. data/lib/crystalruby/templates/function.cr +11 -10
  19. data/lib/crystalruby/templates/index.cr +53 -66
  20. data/lib/crystalruby/templates/inline_chunk.cr +1 -1
  21. data/lib/crystalruby/templates/ruby_interface.cr +34 -0
  22. data/lib/crystalruby/templates/top_level_function.cr +62 -0
  23. data/lib/crystalruby/templates/top_level_ruby_interface.cr +33 -0
  24. data/lib/crystalruby/typebuilder.rb +11 -55
  25. data/lib/crystalruby/typemaps.rb +92 -67
  26. data/lib/crystalruby/types/concerns/allocator.rb +80 -0
  27. data/lib/crystalruby/types/fixed_width/named_tuple.cr +80 -0
  28. data/lib/crystalruby/types/fixed_width/named_tuple.rb +86 -0
  29. data/lib/crystalruby/types/fixed_width/proc.cr +45 -0
  30. data/lib/crystalruby/types/fixed_width/proc.rb +79 -0
  31. data/lib/crystalruby/types/fixed_width/tagged_union.cr +53 -0
  32. data/lib/crystalruby/types/fixed_width/tagged_union.rb +113 -0
  33. data/lib/crystalruby/types/fixed_width/tuple.cr +82 -0
  34. data/lib/crystalruby/types/fixed_width/tuple.rb +92 -0
  35. data/lib/crystalruby/types/fixed_width.cr +138 -0
  36. data/lib/crystalruby/types/fixed_width.rb +205 -0
  37. data/lib/crystalruby/types/primitive.cr +21 -0
  38. data/lib/crystalruby/types/primitive.rb +117 -0
  39. data/lib/crystalruby/types/primitive_types/bool.cr +34 -0
  40. data/lib/crystalruby/types/primitive_types/bool.rb +11 -0
  41. data/lib/crystalruby/types/primitive_types/nil.cr +35 -0
  42. data/lib/crystalruby/types/primitive_types/nil.rb +16 -0
  43. data/lib/crystalruby/types/primitive_types/numbers.cr +37 -0
  44. data/lib/crystalruby/types/primitive_types/numbers.rb +28 -0
  45. data/lib/crystalruby/types/primitive_types/symbol.cr +55 -0
  46. data/lib/crystalruby/types/primitive_types/symbol.rb +35 -0
  47. data/lib/crystalruby/types/primitive_types/time.cr +35 -0
  48. data/lib/crystalruby/types/primitive_types/time.rb +25 -0
  49. data/lib/crystalruby/types/type.cr +64 -0
  50. data/lib/crystalruby/types/type.rb +249 -30
  51. data/lib/crystalruby/types/variable_width/array.cr +74 -0
  52. data/lib/crystalruby/types/variable_width/array.rb +88 -0
  53. data/lib/crystalruby/types/variable_width/hash.cr +146 -0
  54. data/lib/crystalruby/types/variable_width/hash.rb +117 -0
  55. data/lib/crystalruby/types/variable_width/string.cr +36 -0
  56. data/lib/crystalruby/types/variable_width/string.rb +18 -0
  57. data/lib/crystalruby/types/variable_width.cr +23 -0
  58. data/lib/crystalruby/types/variable_width.rb +46 -0
  59. data/lib/crystalruby/types.rb +32 -13
  60. data/lib/crystalruby/version.rb +2 -2
  61. data/lib/crystalruby.rb +13 -6
  62. metadata +42 -22
  63. data/lib/crystalruby/types/array.rb +0 -15
  64. data/lib/crystalruby/types/bool.rb +0 -3
  65. data/lib/crystalruby/types/hash.rb +0 -17
  66. data/lib/crystalruby/types/named_tuple.rb +0 -28
  67. data/lib/crystalruby/types/nil.rb +0 -3
  68. data/lib/crystalruby/types/numbers.rb +0 -5
  69. data/lib/crystalruby/types/string.rb +0 -3
  70. data/lib/crystalruby/types/symbol.rb +0 -3
  71. data/lib/crystalruby/types/time.rb +0 -8
  72. data/lib/crystalruby/types/tuple.rb +0 -17
  73. data/lib/crystalruby/types/type_serializer/json.rb +0 -41
  74. data/lib/crystalruby/types/type_serializer.rb +0 -37
  75. data/lib/crystalruby/types/typedef.rb +0 -57
  76. data/lib/crystalruby/types/union_type.rb +0 -43
  77. data/lib/module.rb +0 -3
@@ -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, :chunks, :root_dir, :lib_dir, :src_dir, :codegen_dir, :reactor
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 main_file, "require \"./#{config.crystal_codegen_dir}/index\"\n" unless File.exist?(main_file)
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 crystalize_method(method, args, returns, function_body, async, &block)
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.define_crystalized_methods!(self)
77
+ func.define_crystallized_methods!(self)
73
78
  func.register_custom_types!(self)
74
- write_chunk(method.owner.name, method.name, func.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 crystalize_chunk(mod, chunk_name, body)
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
- @types_cache ||= {}
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 type_modules
120
- (@types_cache || {}).map do |type_name, expr|
121
- parts = type_name.split("::")
122
- typedef = parts[0...-1].each_with_index.reduce("") do |acc, (part, index)|
123
- acc + "#{" " * index}module #{part}\n"
124
- end
125
- typedef += "#{" " * (parts.size - 1)}alias #{parts.last} = #{expr}\n"
126
- typedef + parts[0...-1].reverse.each_with_index.reduce("") do |acc, (_part, index)|
127
- acc + "#{" " * (parts.size - 2 - index)}end\n"
128
- end
129
- end.join("\n")
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
- chunks.map do |chunk|
134
- chunk_data = chunk[:body]
135
- file_digest = Digest::MD5.hexdigest chunk_data
136
- fname = chunk[:chunk_name]
137
- "require \"./#{chunk[:module_name]}/#{fname}_#{file_digest}.cr\"\n"
138
- end.join("\n")
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
- chunks << { module_name: module_name, chunk_name: chunk_name, body: body }
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
- chunks.each do |chunk|
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
- file_digest = Digest::MD5.hexdigest body
177
- filename = (codegen_dir / module_name / "#{chunk_name}_#{file_digest}.cr").to_s
283
+ file_digest = Digest::MD5.hexdigest body
284
+ filename = (codegen_dir / module_name / "#{chunk_name}_#{file_digest}.cr").to_s
178
285
 
179
- unless current_index_contents.include?("#{module_name}/#{chunk_name}_#{file_digest}.cr")
180
- methods.each_value(&:unattach!)
181
- @compiled = false
182
- end
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
- unless existing.delete(filename)
185
- FileUtils.mkdir_p(codegen_dir / module_name)
186
- File.write(filename, body)
187
- end
188
- existing.select do |f|
189
- f =~ /#{config.crystal_codegen_dir / module_name / "#{chunk_name}_[a-f0-9]{32}\.cr"}/
190
- end.each do |fl|
191
- File.delete(fl) unless fl.eql?(filename)
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
@@ -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 on a single thread.
5
- # Functions annotated with async: true, are executed using callbacks to allow these to be interleaved without blocking.
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
- raise error_type.new(message) unless THREAD_MAP.key?(tid)
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] = error_type.new(message)
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
- raise THREAD_MAP[thread_id][:error] if THREAD_MAP[thread_id][:error]
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
- THREAD_MAP[thread_id][:result]
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
- loop do
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, nil, async: false, blocking: false, lib: lib) if running? && lib
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 current_thread_id=(val)
88
- @current_thread_id = val
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 current_thread_id
92
- @current_thread_id
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
- lambda {
112
- receiver.send(
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
@@ -1,12 +1,23 @@
1
1
  module CrystalRuby
2
2
  module Template
3
- Dir[File.join(File.dirname(__FILE__), "templates", "*.cr")].each do |file|
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
- template_value.define_singleton_method(:render) do |context|
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
- module %{module_name}
7
- def self.%{fn_name}(%{fn_args}) : %{fn_ret_type}
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 = %{module_name}.%{fn_name}(%{arg_names})
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 = %{module_name}.%{fn_name}(%{arg_names})
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.error_callback.call("RuntimeError".to_unsafe, exception.to_unsafe, thread_id)
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.error_callback.call("RuntimeError".to_unsafe, ex.message.to_s.to_unsafe, thread_id)
59
+ CrystalRuby.report_error("RuntimeError", ex.message.to_s, backtrace, thread_id)
59
60
  CrystalRuby.decrement_task_counter
60
61
  })
61
62
  end