crystalruby 0.3.2 → 0.3.4

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 542a695dee21acb928d1c758ce8a48f31422b8b71940b6ea61d87c2744b3fc5c
4
- data.tar.gz: 988da32111ca2a2a13f1b8da93c850a76ea6dda4191f55feabd768b35444e316
3
+ metadata.gz: cccd1a9a08608d1973ec2cca190828d8475652337dbe6ec16b9d7cac1646cdab
4
+ data.tar.gz: 441bf39c0a3627d6df202c54c3440bb69e47c91473c1974b3caa330e0defa7a9
5
5
  SHA512:
6
- metadata.gz: c5109478ed499c9737dc008cae0f720ae03d30a31bffb9add0034303ba7d39b943b437074fb033b52f307cb09f838d64a1a037b36f80bf985e4141614b8ec983
7
- data.tar.gz: 4e1f0831bf0eb15a889ad7ca0bd797ae02377968f41b365593102663ff828b0791cd500871e6ba3145b4613a2ed797690ca0b4866e57ee18d9032ddee546aa96
6
+ metadata.gz: fafb4fecf18aa7e5221642fbb956457987b286d50600f0ced48a91eaf10a4e2ccc83546d2632058b765256fc3510ce17cc5a6aa37455583279fb101a0c56fe70
7
+ data.tar.gz: 77fdc7ebdd02dab88243aec94eb6e06fc0bf2b65685b2eeb0b0304ad3087903879759823045c4ee07c9453715800237c1a230a1fdc7fb0e8eb2bc3636fb90e18
data/CHANGELOG.md CHANGED
@@ -1,3 +1,7 @@
1
+ ## [0.3.4] - 2025-05-04
2
+
3
+ - Added support for Crystal 1.16.0
4
+
1
5
  ## [Unreleased]
2
6
 
3
7
  - Added support for running `crustalruby` without the `crystal` binary in dry mode. [#15]
@@ -36,7 +36,7 @@ module CrystalRuby
36
36
  # @option options [Boolean] :async (false) Mark the method as async (allows multiplexing).
37
37
  # @option options [String] :lib ("crystalruby") The name of the library to compile the Crystal code into.
38
38
  # @option options [Proc] :block An optional wrapper Ruby block that wraps around any invocations of the crystal code
39
- def crystallize( returns=:void, raw: false, async: false, lib: "crystalruby", &block)
39
+ def crystallize(returns = :void, raw: false, async: false, lib: "crystalruby", &block)
40
40
  (self == TOPLEVEL_BINDING.receiver ? Object : self).instance_eval do
41
41
  @crystallize_next = {
42
42
  raw: raw,
@@ -49,7 +49,7 @@ module CrystalRuby
49
49
  end
50
50
 
51
51
  # Alias for `crystallize`
52
- alias :crystalize :crystallize
52
+ alias crystalize crystallize
53
53
 
54
54
  # Exposes a Ruby method to one or more Crystal libraries.
55
55
  # Type annotations follow the same rules as the `crystallize` method, but are
@@ -58,7 +58,7 @@ module CrystalRuby
58
58
  # @param [Hash] options The options hash.
59
59
  # @option options [Boolean] :raw (false) Pass raw Crystal code to the compiler as a string.
60
60
  # @option options [String] :libs (["crystalruby"]) The name of the Crystal librarie(s) to expose the Ruby code to.
61
- def expose_to_crystal( returns=:void, libs: ["crystalruby"])
61
+ def expose_to_crystal(returns = :void, libs: ["crystalruby"])
62
62
  (self == TOPLEVEL_BINDING.receiver ? Object : self).instance_eval do
63
63
  @expose_next_to_crystal = {
64
64
  returns: returns,
@@ -70,7 +70,7 @@ module CrystalRuby
70
70
  # Define a shard dependency
71
71
  # This dependency will be automatically injected into the shard.yml file for
72
72
  # the given library and installed upon compile if it is not already installed.
73
- def shard(shard_name, lib: 'crystalruby', **opts)
73
+ def shard(shard_name, lib: "crystalruby", **opts)
74
74
  CrystalRuby::Library[lib].require_shard(shard_name, **opts)
75
75
  end
76
76
 
@@ -78,14 +78,18 @@ module CrystalRuby
78
78
  # This is useful for defining classes, modules, performing set-up tasks etc.
79
79
  # See: docs for .crystallize to understand the `raw` and `lib` parameters.
80
80
  def crystal(raw: false, lib: "crystalruby", &block)
81
- inline_crystal_body = respond_to?(:name) ? Template::InlineChunk.render(
82
- {
83
- module_name: name,
84
- body: SourceReader.extract_source_from_proc(block, raw: raw),
85
- mod_or_class: self.kind_of?(Class) && self < Types::Type ? "class" : "module",
86
- superclass: self.kind_of?(Class) && self < Types::Type ? "< #{self.crystal_supertype}" : ""
87
- }) :
88
- SourceReader.extract_source_from_proc(block, raw: raw)
81
+ inline_crystal_body = if respond_to?(:name)
82
+ Template::InlineChunk.render(
83
+ {
84
+ module_name: name,
85
+ body: SourceReader.extract_source_from_proc(block, raw: raw),
86
+ mod_or_class: is_a?(Class) && self < Types::Type ? "class" : "module",
87
+ superclass: is_a?(Class) && self < Types::Type ? "< #{crystal_supertype}" : ""
88
+ }
89
+ )
90
+ else
91
+ SourceReader.extract_source_from_proc(block, raw: raw)
92
+ end
89
93
 
90
94
  CrystalRuby::Library[lib].crystallize_chunk(
91
95
  self,
@@ -94,7 +98,6 @@ module CrystalRuby
94
98
  )
95
99
  end
96
100
 
97
-
98
101
  # This method provides a useful DSL for defining Crystal types in pure Ruby
99
102
  # MyType = CRType{ Int32 | Hash(String, Array(Bool) | Float65 | Nil) }
100
103
  # @param [Proc] block The block within which we build the type definition.
@@ -152,14 +155,16 @@ module CrystalRuby
152
155
 
153
156
  owner = method.owner.singleton_class? ? method.owner.attached_object : method.owner
154
157
  owner.class_eval(src)
155
- owner.instance_eval(src) unless method.kind_of?(UnboundMethod) && method.owner.ancestors.include?(CrystalRuby::Types::Type)
156
- method = owner.send(method.kind_of?(UnboundMethod) ? :instance_method : :method, method.name)
158
+ unless method.is_a?(UnboundMethod) && method.owner.ancestors.include?(CrystalRuby::Types::Type)
159
+ owner.instance_eval(src)
160
+ end
161
+ method = owner.send(method.is_a?(UnboundMethod) ? :instance_method : :method, method.name)
157
162
 
158
163
  libs.each do |lib|
159
164
  CrystalRuby::Library[lib].expose_method(
160
165
  method,
161
166
  args,
162
- returns,
167
+ returns
163
168
  )
164
169
  end
165
170
  end
@@ -66,6 +66,7 @@ module CrystalRuby
66
66
  raise ArgumentError, "no block given but function expects block" if !blk && func.takes_block?
67
67
 
68
68
  args << blk if blk
69
+
69
70
  func.map_args!(args)
70
71
  args.unshift(memory) if func.instance_method
71
72
 
@@ -86,27 +87,35 @@ module CrystalRuby
86
87
  def register_callback!
87
88
  return unless ruby
88
89
 
89
- @callback_func = FFI::Function.new(ffi_ret_type, ffi_types) do |*args|
90
+ ret_type = ffi_ret_type == :string ? :pointer : ffi_ret_type
91
+ @callback_func = FFI::Function.new(ret_type, ffi_types) do |*args|
90
92
  receiver = instance_method ? owner.new(args.shift) : owner
91
- ret_val = if takes_block?
92
- block_arg = arg_type_map[:__yield_to][:crystalruby_type].new(args.pop)
93
- receiver.send(name, *unmap_args(args)) do |*args|
94
- args = args.map.with_index do |arg, i|
95
- arg = block_arg.inner_types[i].new(arg) unless arg.is_a?(block_arg.inner_types[i])
96
- arg.memory
97
- end
98
- return_val = block_arg.invoke(*args)
99
- unless return_val.is_a?(block_arg.inner_types[-1])
100
- return_val = block_arg.inner_types[-1].new(return_val)
101
- end
102
- block_arg.inner_types[-1].anonymous? ? return_val.value : return_val
103
- end
104
- else
105
- receiver.send(name, *unmap_args(args))
106
- end
93
+ ret_val = \
94
+ if takes_block?
95
+ block_arg = arg_type_map[:__yield_to][:crystalruby_type].new(args.pop)
96
+ receiver.send(name, *unmap_args(args)) do |*args|
97
+ args = args.map.with_index do |arg, i|
98
+ arg = block_arg.inner_types[i].new(arg) unless arg.is_a?(block_arg.inner_types[i])
99
+ arg.memory
100
+ end
101
+ return_val = block_arg.invoke(*args)
102
+ return_val = block_arg.inner_types[-1].new(return_val) unless return_val.is_a?(block_arg.inner_types[-1])
103
+ block_arg.inner_types[-1].anonymous? ? return_val.value : return_val
104
+ end
105
+ else
106
+ receiver.send(name, *unmap_args(args))
107
+ end
107
108
  unmap_retval(ret_val)
108
109
  end
109
- Reactor.schedule_work!(lib, :"register_#{name.to_s.gsub("?", "q").gsub("=", "eq").gsub("!", "bang")}_callback", @callback_func, :void, blocking: true, async: false)
110
+
111
+ Reactor.schedule_work!(
112
+ lib,
113
+ :"register_#{name.to_s.gsub("?", "q").gsub("=", "eq").gsub("!", "bang")}_callback",
114
+ @callback_func,
115
+ :void,
116
+ blocking: true,
117
+ async: false
118
+ )
110
119
  end
111
120
 
112
121
  # Attaches the crystallized FFI functions to their related Ruby modules and classes.
@@ -245,7 +254,7 @@ module CrystalRuby
245
254
 
246
255
  def register_custom_types!(lib)
247
256
  custom_types.each do |crystalruby_type|
248
- next unless crystalruby_type.is_a?(Class) && crystalruby_type < Types::Type
257
+ next unless Types::Type.subclass?(crystalruby_type)
249
258
 
250
259
  [*crystalruby_type.nested_types].uniq.each do |type|
251
260
  lib.register_type!(type)
@@ -263,7 +272,7 @@ module CrystalRuby
263
272
 
264
273
  mapped = argmap[args[index]]
265
274
  case mapped
266
- when CrystalRuby::Types::Type then
275
+ when CrystalRuby::Types::Type
267
276
  args[index] = mapped.memory
268
277
  (refs ||= []) << mapped
269
278
  else
@@ -291,10 +300,12 @@ module CrystalRuby
291
300
  end
292
301
 
293
302
  def unmap_retval(retval)
303
+ return FFI::MemoryPointer.from_string(retval) if return_type_map[:ffi_ret_type] == :string
294
304
  return retval unless return_type_map[:arg_mapper]
295
305
 
296
306
  retval = return_type_map[:arg_mapper][retval]
297
- retval = retval.memory if retval.kind_of?(CrystalRuby::Types::Type)
307
+
308
+ retval = retval.memory if retval.is_a?(CrystalRuby::Types::Type)
298
309
  retval
299
310
  end
300
311
 
@@ -214,9 +214,7 @@ module CrystalRuby
214
214
  unless compiled?
215
215
  FileUtils.rm_f(lib_file)
216
216
 
217
- if shard_dependencies.any? && shards.empty?
218
- rewrite_shards_file!
219
- end
217
+ rewrite_shards_file! if shard_dependencies.any? && shards.empty?
220
218
 
221
219
  CrystalRuby::Compilation.install_shards!(src_dir)
222
220
  CrystalRuby::Compilation.compile!(
@@ -239,14 +237,16 @@ module CrystalRuby
239
237
  singleton_class.class_eval do
240
238
  extend FFI::Library
241
239
  ffi_lib lib_file
242
- %i[yield init].each do |method_name|
240
+ %i[yield init gc].each do |method_name|
243
241
  singleton_class.undef_method(method_name) if singleton_class.method_defined?(method_name)
244
242
  undef_method(method_name) if method_defined?(method_name)
245
243
  end
246
244
  attach_function :init, %i[string pointer pointer], :void
247
245
  attach_function :yield, %i[], :int
246
+ attach_function :gc, %i[], :void
248
247
  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
248
+ attach_function :"register_#{method.name.to_s.gsub("?", "q").gsub("=", "eq").gsub("!", "bang")}_callback",
249
+ %i[pointer], :void
250
250
  end
251
251
  end
252
252
 
@@ -1,5 +1,6 @@
1
+ require "json"
2
+
1
3
  module CrystalRuby
2
- require 'json'
3
4
  # The Reactor represents a singleton Thread responsible for running all Ruby/crystal interop code.
4
5
  # Crystal's Fiber scheduler and GC assume all code is run on a single thread.
5
6
  # This class is responsible for multiplexing Ruby and Crystal code onto a single thread.
@@ -9,12 +10,21 @@ module CrystalRuby
9
10
  module_function
10
11
 
11
12
  class SingleThreadViolation < StandardError; end
13
+
12
14
  class StopReactor < StandardError; end
13
15
 
16
+ @op_count = 0
14
17
  @single_thread_mode = false
15
18
 
16
19
  REACTOR_QUEUE = Queue.new
17
20
 
21
+ # Invoke GC every 100 ops
22
+ GC_OP_THRESHOLD = ENV.fetch("CRYSTAL_GC_OP_THRESHOLD", 100).to_i
23
+ # Or every 0.05 seconds
24
+ GC_INTERVAL = ENV.fetch("CRYSTAL_GC_INTERVAL", 0.05).to_f
25
+ # Or if we've gotten hold of a reference to at least 100KB or more of fresh memory since last GC
26
+ GC_BYTES_SEEN_THRESHOLD = ENV.fetch("CRYSTAL_GC_BYTES_SEEN_THRESHOLD", 100 * 1024).to_i
27
+
18
28
  # We maintain a map of threads, each with a mutex, condition variable, and result
19
29
  THREAD_MAP = Hash.new do |h, tid_or_thread, tid = tid_or_thread|
20
30
  if tid_or_thread.is_a?(Thread)
@@ -36,7 +46,7 @@ module CrystalRuby
36
46
 
37
47
  # We memoize callbacks, once per return type
38
48
  CALLBACKS_MAP = Hash.new do |h, rt|
39
- h[rt] = FFI::Function.new(:void, [:int, *(rt == :void ? [] : [rt])]) do |tid, ret|
49
+ h[rt] = FFI::Function.new(:void, [:int, *((rt == :void) ? [] : [rt])]) do |tid, ret|
40
50
  THREAD_MAP[tid][:error] = nil
41
51
  THREAD_MAP[tid][:result] = ret
42
52
  THREAD_MAP[tid][:cond].signal
@@ -49,7 +59,7 @@ module CrystalRuby
49
59
  error_type = is_exception_type ? Object.const_get(error_type) : RuntimeError
50
60
  error = error_type.new(message)
51
61
  error.set_backtrace(JSON.parse(backtrace))
52
- raise error unless THREAD_MAP.key?(tid)
62
+ raise error unless THREAD_MAP.key?(tid)
53
63
 
54
64
  THREAD_MAP[tid][:error] = error
55
65
  THREAD_MAP[tid][:result] = nil
@@ -62,10 +72,12 @@ module CrystalRuby
62
72
 
63
73
  def await_result!
64
74
  mux, cond, result, err = thread_conditions.values_at(:mux, :cond, :result, :error)
65
- cond.wait(mux) unless (result || err)
75
+ cond.wait(mux) unless result || err
66
76
  result, err, thread_conditions[:result], thread_conditions[:error] = thread_conditions.values_at(:result, :error)
67
77
  if err
68
- combined_backtrace = err.backtrace[0..(err.backtrace.index{|m| m.include?('call_blocking_function')} || 2) - 3] + caller[5..-1]
78
+ combined_backtrace = err.backtrace[0..(err.backtrace.index { |m|
79
+ m.include?("call_blocking_function")
80
+ } || 2) - 3] + caller[5..-1]
69
81
  err.set_backtrace(combined_backtrace)
70
82
  raise err
71
83
  end
@@ -78,30 +90,74 @@ module CrystalRuby
78
90
  end
79
91
 
80
92
  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
93
+ return unless @main_loop
94
+
95
+ schedule_work!(self, :halt_loop!, :void, blocking: true, async: false)
96
+ @main_loop.join
97
+ @main_loop = nil
98
+ CrystalRuby.log_info "Reactor loop stopped"
87
99
  end
88
100
 
89
101
  def start!
102
+ @op_count = 0
90
103
  @main_loop ||= Thread.new do
91
104
  @main_thread_id = Thread.current.object_id
92
105
  CrystalRuby.log_debug("Starting reactor")
93
106
  CrystalRuby.log_debug("CrystalRuby initialized")
94
107
  while true
95
- handler, *args = REACTOR_QUEUE.pop
96
- send(handler, *args)
108
+ handler, *args, lib = REACTOR_QUEUE.pop
109
+ send(handler, *args, lib)
110
+ @op_count += 1
111
+ invoke_gc_if_due!(lib)
97
112
  end
98
- rescue StopReactor => e
99
- rescue StandardError => e
113
+ rescue StopReactor
114
+ rescue => e
100
115
  CrystalRuby.log_error "Error: #{e}"
101
116
  CrystalRuby.log_error e.backtrace
102
117
  end
103
118
  end
104
119
 
120
+ def invoke_gc_if_due!(lib)
121
+ schedule_work!(lib, :gc, :void, blocking: true, async: false, lib: lib) if lib && gc_due?
122
+ end
123
+
124
+ def gc_due?
125
+ now = Process.clock_gettime(Process::CLOCK_MONOTONIC)
126
+
127
+ # Initialize state variables if not already set.
128
+ @last_gc_time ||= now
129
+ @op_count ||= 0
130
+ @last_gc_op_count ||= @op_count
131
+ @last_mem_check_time ||= now
132
+
133
+ # Calculate differences based on ops and time.
134
+ ops_since_last_gc = @op_count - @last_gc_op_count
135
+ time_since_last_gc = now - @last_gc_time
136
+
137
+ # Start with our two “cheap” conditions.
138
+ due = (ops_since_last_gc >= GC_OP_THRESHOLD) || (time_since_last_gc >= GC_INTERVAL) || Types::Allocator.gc_bytes_seen > GC_BYTES_SEEN_THRESHOLD
139
+
140
+ if due
141
+ # Update the baseline values after GC is scheduled.
142
+ @last_gc_time = now
143
+ # If we just did a memory check, use that value; otherwise, fetch one now.
144
+ @last_gc_op_count = @op_count
145
+ Types::Allocator.gc_hint_reset!
146
+ true
147
+ else
148
+ false
149
+ end
150
+ end
151
+
152
+ def start_gc_thread!(lib)
153
+ Thread.new do
154
+ loop do
155
+ schedule_work!(lib, :gc, :void, blocking: true, async: false, lib: lib) if gc_due?
156
+ sleep GC_INTERVAL
157
+ end
158
+ end
159
+ end
160
+
105
161
  def thread_id
106
162
  Thread.current.object_id
107
163
  end
@@ -116,14 +172,14 @@ module CrystalRuby
116
172
  yield!(lib: lib, time: 0)
117
173
  end
118
174
 
119
- def invoke_blocking!(receiver, op_name, *args, tvars)
175
+ def invoke_blocking!(receiver, op_name, *args, tvars, _lib)
120
176
  tvars[:error] = nil
121
177
  begin
122
178
  tvars[:result] = receiver.send(op_name, *args)
123
- rescue StopReactor => e
179
+ rescue StopReactor
124
180
  tvars[:cond].signal
125
181
  raise
126
- rescue StandardError => e
182
+ rescue => e
127
183
  tvars[:error] = e
128
184
  end
129
185
  tvars[:cond].signal
@@ -138,9 +194,10 @@ module CrystalRuby
138
194
  if @single_thread_mode || (Thread.current.object_id == @main_thread_id && op_name != :yield)
139
195
  unless Thread.current.object_id == @main_thread_id
140
196
  raise SingleThreadViolation,
141
- "Single thread mode is enabled, cannot run in multi-threaded mode. " \
142
- "Reactor was started from: #{@main_thread_id}, then called from #{Thread.current.object_id}"
197
+ "Single thread mode is enabled, cannot run in multi-threaded mode. " \
198
+ "Reactor was started from: #{@main_thread_id}, then called from #{Thread.current.object_id}"
143
199
  end
200
+ invoke_gc_if_due!(lib)
144
201
  return receiver.send(op_name, *args)
145
202
  end
146
203
 
@@ -149,7 +206,7 @@ module CrystalRuby
149
206
  REACTOR_QUEUE.push(
150
207
  case true
151
208
  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]
209
+ when blocking then [:invoke_blocking!, receiver, op_name, *args, tvars, lib]
153
210
  else [:invoke_await!, receiver, op_name, *args, lib]
154
211
  end
155
212
  )
@@ -4,12 +4,12 @@ module CrystalRuby
4
4
 
5
5
  # Reads code line by line from a given source location and returns the first valid Ruby expression found
6
6
  def extract_expr_from_source_location(source_location)
7
- lines = source_location.then{|f,l| IO.readlines(f)[l-1..]}
7
+ lines = source_location.then { |f, l| IO.readlines(f)[l - 1..] }
8
8
  lines[0] = lines[0][/CRType.*/] if lines[0] =~ /<\s+CRType/ || lines[0] =~ /= CRType/
9
9
  lines.each.with_object([]) do |line, expr_source|
10
- break expr_source.join("") if (Prism.parse((expr_source << line).join("")).success?)
10
+ break expr_source.join("") if Prism.parse((expr_source << line).join("")).success?
11
11
  end
12
- rescue
12
+ rescue StandardError
13
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
14
  end
15
15
 
@@ -27,26 +27,25 @@ module CrystalRuby
27
27
  block_source = extract_expr_from_source_location(block.source_location)
28
28
  parsed_source = Prism.parse(block_source).value
29
29
 
30
- node = parsed_source.statements.body[0].arguments&.arguments&.find{|x| search_node(x, Prism::StatementsNode) }
30
+ node = parsed_source.statements.body[0].arguments&.arguments&.find { |x| search_node(x, Prism::StatementsNode) }
31
31
  node ||= parsed_source.statements.body[0]
32
- body_node = search_node(node, Prism::StatementsNode)
32
+ body_node = search_node(node, Prism::StatementsNode)
33
33
 
34
- return raw ?
35
- extract_raw_string_node(body_node) :
36
- node_to_s(body_node)
34
+ raw ? extract_raw_string_node(body_node) : node_to_s(body_node)
37
35
  end
38
36
 
39
37
  def extract_raw_string_node(node)
40
- search_node(node, Prism::InterpolatedStringNode)&.parts&.map(&:unescaped)&.join("") ||
41
- search_node(node, Prism::StringNode).unescaped
38
+ search_node(node, Prism::InterpolatedStringNode)&.parts&.map do |p|
39
+ p.respond_to?(:unescaped) ? p.unescaped : p.slice
40
+ end&.join("") ||
41
+ search_node(node, Prism::StringNode).unescaped
42
42
  end
43
43
 
44
-
45
44
  # Simple helper function to turn a SyntaxTree node back into a Ruby string
46
45
  # The default formatter will turn a break/return of [1,2,3] into a brackless 1,2,3
47
46
  # Can't have that in Crystal as it turns it into a Tuple
48
47
  def node_to_s(node)
49
- node&.slice || ''
48
+ node&.slice || ""
50
49
  end
51
50
 
52
51
  # Given a method, extracts the source code of the block passed to it
@@ -69,14 +68,14 @@ module CrystalRuby
69
68
  def extract_args_and_source_from_method(method, raw: false)
70
69
  method_source = extract_expr_from_source_location(method.source_location)
71
70
  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
71
+ params = search_node(parsed_source, Prism::ParametersNode)
72
+ args = params ? params.keywords.map { |kw| [kw.name, node_to_s(kw.value)] }.to_h : {}
73
+ body_node = parsed_source.statements.body[0].body
75
74
  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}
75
+ wrapped = %(begin\n#{body_node.statements.slice}\n#{body_node.rescue_clause.slice}\nend)
77
76
  body_node = Prism.parse(wrapped).value
78
77
  end
79
- body = raw ? extract_raw_string_node(body_node) : node_to_s(body_node)
78
+ body = raw ? extract_raw_string_node(body_node) : node_to_s(body_node)
80
79
 
81
80
  args.transform_values! do |type_exp|
82
81
  if CrystalRuby::Typemaps::CRYSTAL_TYPE_MAP.key?(type_exp[1..-1].to_sym)
@@ -85,8 +84,7 @@ module CrystalRuby
85
84
  TypeBuilder.build_from_source(type_exp, context: method.owner)
86
85
  end
87
86
  end.to_h
88
- return args, body
87
+ [args, body]
89
88
  end
90
-
91
89
  end
92
90
  end
@@ -25,9 +25,12 @@ module CrystalRuby
25
25
  return if self.initialized
26
26
  self.initialized = true
27
27
  argv_ptr = ARGV1.to_unsafe
28
+ {%% if compare_versions(Crystal::VERSION, "1.16.0") >= 0 %%}
29
+ Crystal.init_runtime
30
+ {%% end %%}
28
31
  Crystal.main_user_code(0, pointerof(argv_ptr))
29
32
  self.libname = String.new(libname)
30
- LibGC.set_finalize_on_demand(1)
33
+ GC.init
31
34
  end
32
35
 
33
36
  # Explicit error handling (triggers exception within Ruby on the same thread)
@@ -57,7 +60,7 @@ module CrystalRuby
57
60
  self.callbacks.send(callback)
58
61
  end
59
62
 
60
- def self.synchronize
63
+ def self.synchronize(&)
61
64
  LibC.pthread_mutex_lock(self.rc_mux)
62
65
  yield
63
66
  LibC.pthread_mutex_unlock(self.rc_mux)
@@ -100,6 +103,11 @@ module GC
100
103
  end
101
104
  end
102
105
 
106
+ # Trigger GC
107
+ fun gc : Void
108
+ GC.collect
109
+ end
110
+
103
111
  # Yield to the Crystal scheduler from Ruby
104
112
  # If there's callbacks to process, we flush them
105
113
  # Otherwise, we yield to the Crystal scheduler and let Ruby know
@@ -3,31 +3,30 @@
3
3
  module CrystalRuby
4
4
  module Typemaps
5
5
  CRYSTAL_TYPE_MAP = {
6
- char: "Int8", # In Crystal, :char is typically represented as Int8
7
- uchar: "UInt8", # Unsigned char
8
- int8: "Int8", # Same as :char
9
- uint8: "UInt8", # Same as :uchar
10
- short: "Int16", # Short integer
11
- ushort: "UInt16", # Unsigned short integer
12
- int16: "Int16", # Same as :short
13
- uint16: "UInt16", # Same as :ushort
14
- int: "Int32", # Integer, Crystal defaults to 32 bits
15
- uint: "UInt32", # Unsigned integer
16
- int32: "Int32", # 32-bit integer
17
- uint32: "UInt32", # 32-bit unsigned integer
18
- long: "Int32 | Int64", # Long integer, size depends on the platform (32 or 64 bits)
19
- ulong: "UInt32 | UInt64", # Unsigned long integer, size depends on the platform
20
- int64: "Int64", # 64-bit integer
21
- uint64: "UInt64", # 64-bit unsigned integer
22
- long_long: "Int64", # Same as :int64
23
- ulong_long: "UInt64", # Same as :uint64
24
- float: "Float32", # Floating point number (single precision)
25
- double: "Float64", # Double precision floating point number
26
- bool: "Bool", # Boolean type
27
- void: "Void", # Void type
28
- string: "String", # String type
29
- pointer: "Pointer(Void)" # Pointer type
30
-
6
+ char: "::Int8", # In Crystal, :char is typically represented as Int8
7
+ uchar: "::UInt8", # Unsigned char
8
+ int8: "::Int8", # Same as :char
9
+ uint8: "::UInt8", # Same as :uchar
10
+ short: "::Int16", # Short integer
11
+ ushort: "::UInt16", # Unsigned short integer
12
+ int16: "::Int16", # Same as :short
13
+ uint16: "::UInt16", # Same as :ushort
14
+ int: "::Int32", # Integer, Crystal defaults to 32 bits
15
+ uint: "::UInt32", # Unsigned integer
16
+ int32: "::Int32", # 32-bit integer
17
+ uint32: "::UInt32", # 32-bit unsigned integer
18
+ long: "::Int32 | Int64", # Long integer, size depends on the platform (32 or 64 bits)
19
+ ulong: "::UInt32 | UInt64", # Unsigned long integer, size depends on the platform
20
+ int64: "::Int64", # 64-bit integer
21
+ uint64: "::UInt64", # 64-bit unsigned integer
22
+ long_long: "::Int64", # Same as :int64
23
+ ulong_long: "::UInt64", # Same as :uint64
24
+ float: "::Float32", # Floating point number (single precision)
25
+ double: "::Float64", # Double precision floating point number
26
+ bool: "::Bool", # Boolean type
27
+ void: "::Void", # Void type
28
+ string: "::String", # String type
29
+ pointer: "::Pointer(Void)" # Pointer type
31
30
  }
32
31
 
33
32
  FFI_TYPE_MAP = CRYSTAL_TYPE_MAP.invert
@@ -61,13 +60,13 @@ module CrystalRuby
61
60
 
62
61
  C_TYPE_MAP = CRYSTAL_TYPE_MAP.merge(
63
62
  {
64
- string: "Pointer(UInt8)"
63
+ string: "Pointer(::UInt8)"
65
64
  }
66
65
  )
67
66
 
68
67
  C_TYPE_CONVERSIONS = {
69
68
  string: {
70
- from: "String.new(%s)",
69
+ from: "::String.new(%s.not_nil!)",
71
70
  to: "%s.to_unsafe"
72
71
  },
73
72
  void: {
@@ -86,6 +85,11 @@ module CrystalRuby
86
85
 
87
86
  def build_type_map(crystalruby_type)
88
87
  crystalruby_type = CRType(&crystalruby_type) if crystalruby_type.is_a?(Proc)
88
+
89
+ if Types::Type.subclass?(crystalruby_type) && crystalruby_type.ffi_primitive_type
90
+ crystalruby_type = crystalruby_type.ffi_primitive_type
91
+ end
92
+
89
93
  {
90
94
  ffi_type: ffi_type(crystalruby_type),
91
95
  ffi_ret_type: ffi_type(crystalruby_type),
@@ -93,19 +97,25 @@ module CrystalRuby
93
97
  crystalruby_type: crystalruby_type,
94
98
  lib_type: lib_type(crystalruby_type),
95
99
  error_value: error_value(crystalruby_type),
96
- arg_mapper: if crystalruby_type.is_a?(Class) && crystalruby_type < Types::Type
100
+ arg_mapper: if Types::Type.subclass?(crystalruby_type)
97
101
  lambda { |arg|
98
102
  arg = crystalruby_type.new(arg.memory) if arg.is_a?(Types::Type) && !arg.is_a?(crystalruby_type)
99
103
  arg = crystalruby_type.new(arg) unless arg.is_a?(Types::Type)
104
+
105
+ Types::FixedWidth.increment_ref_count!(arg.memory) if arg.class < Types::FixedWidth
106
+
100
107
  arg
101
108
  }
102
109
  end,
103
- retval_mapper: if crystalruby_type.is_a?(Class) && crystalruby_type < Types::Type
110
+ retval_mapper: if Types::Type.subclass?(crystalruby_type)
104
111
  lambda { |arg|
105
112
  if arg.is_a?(Types::Type) && !arg.is_a?(crystalruby_type)
106
113
  arg = crystalruby_type.new(arg.memory)
107
114
  end
108
115
  arg = crystalruby_type.new(arg) unless arg.is_a?(Types::Type)
116
+
117
+ Types::FixedWidth.decrement_ref_count!(arg.memory) if arg.class < Types::FixedWidth
118
+
109
119
  crystalruby_type.anonymous? ? arg.native : arg
110
120
  }
111
121
  # Strings in Crystal are UTF-8 encoded by default
@@ -2,9 +2,21 @@ module CrystalRuby
2
2
  module Types
3
3
  # Module for memory allocation and tracking functionality
4
4
  module Allocator
5
-
6
5
  # Called when module is included in a class
7
6
  # @param base [Class] The class including this module
7
+
8
+ def self.gc_hint!(size)
9
+ @bytes_seen_since_gc = (@bytes_seen_since_gc || 0) + size
10
+ end
11
+
12
+ def self.gc_bytes_seen
13
+ @bytes_seen_since_gc ||= 0
14
+ end
15
+
16
+ def self.gc_hint_reset!
17
+ @bytes_seen_since_gc = 0
18
+ end
19
+
8
20
  def self.included(base)
9
21
  base.class_eval do
10
22
  # Synchronizes a block using mutex
@@ -21,7 +33,7 @@ module CrystalRuby
21
33
 
22
34
  extend FFI::Library
23
35
  ffi_lib "c"
24
- attach_function :_calloc, :calloc, [:size_t, :size_t], :pointer
36
+ attach_function :_calloc, :calloc, %i[size_t size_t], :pointer
25
37
  attach_function :_free, :free, [:pointer], :void
26
38
  define_singleton_method(:ptr, &FFI::Pointer.method(:new))
27
39
  define_method(:ptr, &FFI::Pointer.method(:new))
@@ -4,7 +4,8 @@ module CrystalRuby::Types
4
4
  "and a single return type (or Nil if it does not return a value)")
5
5
 
6
6
  def self.Proc(*types)
7
- proc_type = FixedWidth.build(:Proc, convert_if: [::Proc], inner_types: types, ffi_type: :pointer) do
7
+ proc_type = FixedWidth.build(:Proc, convert_if: [::Proc], inner_types: types,
8
+ ffi_type: :pointer) do
8
9
  @data_offset = 4
9
10
 
10
11
  def self.cast!(rbval)
@@ -57,12 +58,12 @@ module CrystalRuby::Types
57
58
  result = nil
58
59
  if Fiber.current == Thread.current.main_fiber
59
60
  block_value = #{inner_types[-1].crystal_class_name}.new(__yield_to.call(#{inner_types.size.-(1).times.map { |i| "v#{i}" }.join(",")}))
60
- result = #{inner_types[-1].anonymous? ? "block_value.native" : "block_value"}
61
+ result = #{inner_types[-1].anonymous? ? "block_value.native_decr" : "block_value"}
61
62
  next #{inner_types.last == CrystalRuby::Types::Nil ? "result" : "result.not_nil!"}
62
63
  else
63
64
  CrystalRuby.queue_callback(->{
64
65
  block_value = #{inner_types[-1].crystal_class_name}.new(__yield_to.call(#{inner_types.size.-(1).times.map { |i| "v#{i}" }.join(",")}))
65
- result = #{inner_types[-1].anonymous? ? "block_value.native" : "block_value"}
66
+ result = #{inner_types[-1].anonymous? ? "block_value.native_decr" : "block_value"}
66
67
  callback_done_channel.send(nil)
67
68
  })
68
69
  end
@@ -57,6 +57,11 @@ module CrystalRuby::Types
57
57
  union_types
58
58
  end
59
59
 
60
+ def total_memsize
61
+ type = self.class.union_types[data_pointer.read_uint8]
62
+ memsize + refsize + (type.primitive? ? type.memsize : value.total_memsize)
63
+ end
64
+
60
65
  define_singleton_method(:memsize) do
61
66
  union_types.map(&:refsize).max + 1
62
67
  end
@@ -35,6 +35,17 @@ module CrystalRuby
35
35
  8
36
36
  end
37
37
 
38
+ def self.new_decr(arg)
39
+ new_value = self.new(arg)
40
+ self.decrement_ref_count!(new_value.memory)
41
+ new_value
42
+ end
43
+
44
+ def native_decr
45
+ self.class.decrement_ref_count!(@memory)
46
+ native
47
+ end
48
+
38
49
 
39
50
  def self.free!(memory)
40
51
  # Decrease ref counts for any data we are pointing to
@@ -66,6 +77,13 @@ module CrystalRuby
66
77
  memory.as(Pointer(::UInt32))[0] = value
67
78
  end
68
79
 
80
+ # When we pass to Ruby, we increment the ref count
81
+ # for Ruby to decrement again once it receives.
82
+ def return_value
83
+ FixedWidth.increment_ref_count!(memory)
84
+ memory
85
+ end
86
+
69
87
  # Data pointer follows the ref count (and size for variable width types)
70
88
  # In the case of variable width types the data pointer points to the start of a separate data block
71
89
  # So this method is overridden inside variable_width.rb to resolve this pointer.
@@ -12,13 +12,14 @@ module CrystalRuby
12
12
  else allocate_new_from_value!(rbval)
13
13
  end
14
14
  self.class.increment_ref_count!(memory)
15
- ObjectSpace.define_finalizer(self, self.class.finalize(memory))
15
+ ObjectSpace.define_finalizer(self, self.class.finalize(memory, self.class))
16
+ Allocator.gc_hint!(total_memsize)
16
17
  end
17
18
 
18
- def self.finalize(memory)
19
- lambda { |_|
19
+ def self.finalize(memory, type)
20
+ lambda do |_|
20
21
  decrement_ref_count!(memory)
21
- }
22
+ end
22
23
  end
23
24
 
24
25
  def allocate_new_from_value!(rbval)
@@ -137,6 +138,10 @@ module CrystalRuby
137
138
  memory[size_offset].read_int32
138
139
  end
139
140
 
141
+ def total_memsize
142
+ memsize + refsize + size
143
+ end
144
+
140
145
  def address
141
146
  @memory.address
142
147
  end
@@ -180,6 +185,7 @@ module CrystalRuby
180
185
  superclass: FixedWidth,
181
186
  size_offset: 4,
182
187
  data_offset: 4,
188
+ ffi_primitive: false,
183
189
  &block
184
190
  )
185
191
  inner_types&.each(&Type.method(:validate!))
@@ -187,7 +193,7 @@ module CrystalRuby
187
193
  Class.new(superclass) do
188
194
  bind_local_vars!(
189
195
  %i[typename error inner_types inner_keys ffi_type memsize convert_if size_offset data_offset
190
- refsize], binding
196
+ refsize ffi_primitive], binding
191
197
  )
192
198
  class_eval(&block) if block_given?
193
199
 
@@ -60,11 +60,12 @@ module CrystalRuby
60
60
  memsize: FFI.type_size(ffi_type),
61
61
  convert_if: [],
62
62
  error: nil,
63
+ ffi_primitive: false,
63
64
  superclass: Primitive,
64
65
  &block
65
66
  )
66
67
  Class.new(superclass) do
67
- %w[typename ffi_type memsize convert_if error].each do |name|
68
+ %w[typename ffi_type memsize convert_if error ffi_primitive].each do |name|
68
69
  define_singleton_method(name) { binding.local_variable_get("#{name}") }
69
70
  define_method(name) { binding.local_variable_get("#{name}") }
70
71
  end
@@ -1,7 +1,7 @@
1
1
  module CrystalRuby::Types
2
2
  %i[UInt8 UInt16 UInt32 UInt64 Int8 Int16 Int32 Int64 Float32 Float64].each do |type_name|
3
- ffi_type = CrystalRuby::Typemaps::FFI_TYPE_MAP.fetch(type_name.to_s)
4
- const_set(type_name, Primitive.build(type_name, convert_if: [::Numeric], ffi_type: ffi_type) do
3
+ ffi_type = CrystalRuby::Typemaps::FFI_TYPE_MAP.fetch("::#{type_name}")
4
+ const_set(type_name, Primitive.build(type_name, convert_if: [::Numeric], ffi_type: ffi_type, ffi_primitive: ffi_type) do
5
5
  def value=(val)
6
6
  raise "Expected a numeric value, got #{val}" unless val.is_a?(::Numeric)
7
7
 
@@ -30,6 +30,10 @@ module CrystalRuby::Types
30
30
  define_singleton_method(:type_digest) do
31
31
  Digest::MD5.hexdigest(native_type_expr.to_s + allowed_values.map(&:to_s).join(","))
32
32
  end
33
+
34
+ def self.ffi_primitive_type
35
+ nil
36
+ end
33
37
  end
34
38
  end
35
39
  end
@@ -17,6 +17,14 @@ module CrystalRuby
17
17
  def to_s
18
18
  native.to_s
19
19
  end
20
+
21
+ def self.new_decr(arg)
22
+ self.new(arg)
23
+ end
24
+
25
+ def native_decr
26
+ native
27
+ end
20
28
 
21
29
  def synchronize
22
30
  CrystalRuby.synchronize do
@@ -47,7 +47,7 @@ module CrystalRuby
47
47
  :write_mixed_byte_slices_to_uint8_array, :data_offset, :size_offset,
48
48
  :union_types
49
49
 
50
- attr_accessor :value, :memory
50
+ attr_accessor :value, :memory, :ffi_primitive
51
51
 
52
52
  def initialize(_rbval)
53
53
  @class = self.class
@@ -133,7 +133,7 @@ module CrystalRuby
133
133
  end
134
134
 
135
135
  def self.pointer_to_crystal_type_conversion(expr)
136
- anonymous? ? "#{crystal_class_name}.new(#{expr}).native" : "#{crystal_class_name}.new(#{expr})"
136
+ anonymous? ? "#{crystal_class_name}.new(#{expr}).native_decr" : "#{crystal_class_name}.new_decr(#{expr})"
137
137
  end
138
138
 
139
139
  def self.crystal_type_to_pointer_type_conversion(expr)
@@ -243,6 +243,17 @@ module CrystalRuby
243
243
  inner_types.map(&:memsize).sum
244
244
  end
245
245
 
246
+ def total_memsize
247
+ memsize
248
+ end
249
+
250
+ # For non-container ffi_primitive non-named types,
251
+ # just use the raw FFI type, as it's much more efficient
252
+ # due to skipping Arc overhead.
253
+ def self.ffi_primitive_type
254
+ respond_to?(:ffi_primitive) && anonymous? ? ffi_primitive : nil
255
+ end
256
+
246
257
  def self.crystal_type
247
258
  lib_type(ffi_type)
248
259
  end
@@ -265,6 +276,10 @@ module CrystalRuby
265
276
  inner_types.first
266
277
  end
267
278
 
279
+ def self.subclass?(type)
280
+ type.is_a?(Class) && type < Types::Type
281
+ end
282
+
268
283
  def self.type_expr
269
284
  if !inner_types
270
285
  inspect_name
@@ -1,5 +1,5 @@
1
1
  module CrystalRuby::Types
2
- String = VariableWidth.build(:String, convert_if: [String, Root::String]) do
2
+ String = VariableWidth.build(:String, ffi_primitive: :string, convert_if: [String, Root::String]) do
3
3
  def self.cast!(rbval)
4
4
  rbval.to_s
5
5
  end
@@ -19,6 +19,7 @@ module CrystalRuby
19
19
  inner_types: nil,
20
20
  inner_keys: nil,
21
21
  ffi_type: :pointer,
22
+ ffi_primitive: false,
22
23
  size_offset: 4,
23
24
  data_offset: 8,
24
25
  memsize: FFI.type_size(ffi_type),
@@ -32,7 +33,7 @@ module CrystalRuby
32
33
  Class.new(superclass) do
33
34
  bind_local_vars!(
34
35
  %i[typename error inner_types inner_keys ffi_type memsize convert_if data_offset size_offset
35
- refsize], binding
36
+ refsize ffi_primitive], binding
36
37
  )
37
38
  class_eval(&block) if block_given?
38
39
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module CrystalRuby
4
- VERSION = "0.3.2"
4
+ VERSION = "0.3.4"
5
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: crystalruby
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.2
4
+ version: 0.3.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - Wouter Coppieters
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2024-12-22 00:00:00.000000000 Z
11
+ date: 2025-05-03 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: digest
@@ -42,30 +42,36 @@ dependencies:
42
42
  name: fileutils
43
43
  requirement: !ruby/object:Gem::Requirement
44
44
  requirements:
45
- - - ">="
45
+ - - "~>"
46
46
  - !ruby/object:Gem::Version
47
- version: '0'
47
+ version: '1.7'
48
48
  type: :runtime
49
49
  prerelease: false
50
50
  version_requirements: !ruby/object:Gem::Requirement
51
51
  requirements:
52
- - - ">="
52
+ - - "~>"
53
53
  - !ruby/object:Gem::Version
54
- version: '0'
54
+ version: '1.7'
55
55
  - !ruby/object:Gem::Dependency
56
56
  name: prism
57
57
  requirement: !ruby/object:Gem::Requirement
58
58
  requirements:
59
59
  - - ">="
60
60
  - !ruby/object:Gem::Version
61
- version: '0'
61
+ version: 1.3.0
62
+ - - "<"
63
+ - !ruby/object:Gem::Version
64
+ version: 1.5.0
62
65
  type: :runtime
63
66
  prerelease: false
64
67
  version_requirements: !ruby/object:Gem::Requirement
65
68
  requirements:
66
69
  - - ">="
67
70
  - !ruby/object:Gem::Version
68
- version: '0'
71
+ version: 1.3.0
72
+ - - "<"
73
+ - !ruby/object:Gem::Version
74
+ version: 1.5.0
69
75
  description: Embed Crystal code directly in Ruby.
70
76
  email:
71
77
  - wc@pico.net.nz
@@ -82,7 +88,6 @@ files:
82
88
  - LICENSE.txt
83
89
  - README.md
84
90
  - Rakefile
85
- - crystalruby.gemspec
86
91
  - examples/adder/adder.rb
87
92
  - exe/crystalruby
88
93
  - lib/crystalruby.rb
data/crystalruby.gemspec DELETED
@@ -1,41 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require_relative "lib/crystalruby/version"
4
-
5
- Gem::Specification.new do |spec|
6
- spec.name = "crystalruby"
7
- spec.version = CrystalRuby::VERSION
8
- spec.authors = ["Wouter Coppieters"]
9
- spec.email = ["wc@pico.net.nz"]
10
-
11
- spec.summary = "Embed Crystal code directly in Ruby."
12
- spec.description = "Embed Crystal code directly in Ruby."
13
- spec.homepage = "https://github.com/wouterken/crystalruby"
14
- spec.license = "MIT"
15
- spec.required_ruby_version = ">= 2.7.2"
16
-
17
- spec.metadata["homepage_uri"] = spec.homepage
18
- spec.metadata["source_code_uri"] = spec.homepage
19
- spec.metadata["changelog_uri"] = "#{spec.homepage}/CHANGELOG.md"
20
-
21
- # Specify which files should be added to the gem when it is released.
22
- # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
23
- spec.files = Dir.chdir(__dir__) do
24
- `git ls-files -z`.split("\x0").reject do |f|
25
- (File.expand_path(f) == __FILE__) ||
26
- f.start_with?(*%w[bin/ test/ spec/ features/ .git appveyor Gemfile])
27
- end
28
- end
29
- spec.bindir = "exe"
30
- spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
31
- spec.require_paths = ["lib"]
32
-
33
- # Uncomment to register a new dependency of your gem
34
- # spec.add_dependency "example-gem", "~> 1.0"
35
- spec.add_dependency "digest"
36
- spec.add_dependency "ffi"
37
- spec.add_dependency "fileutils"
38
- spec.add_dependency "prism"
39
- # For more information and examples about making a new gem, check out our
40
- # guide at: https://bundler.io/guides/creating_gem.html
41
- end