crystalruby 0.2.3 → 0.3.1

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