lib/jsi/util.rb in jsi-0.4.0 vs lib/jsi/util.rb in jsi-0.6.0
- old
+ new
@@ -1,15 +1,15 @@
# frozen_string_literal: true
module JSI
- # JSI::Util classes, modules, constants, and methods are INTERNAL and will be added and removed without warning.
- # do not rely on them.
+ # JSI::Util classes, modules, constants, and methods are internal, and will be added and removed without warning.
+ #
+ # @api private
module Util
- # a proc which does nothing
- NOOP = -> (*_) { }
+ autoload :AttrStruct, 'jsi/util/attr_struct'
- # returns a version of the given hash, in which any symbol keys are
+ # a hash copied from the given hashlike, in which any symbol keys are
# converted to strings. behavior on collisions is undefined (but in the
# future could take a block like
# ActiveSupport::HashWithIndifferentAccess#update)
#
# at the moment it is undefined whether the returned hash is the same
@@ -52,57 +52,173 @@
else
object
end
end
+ # ensures the given param becomes a frozen Set of Modules.
+ # returns the param if it is already that, otherwise initializes and freezes such a Set.
+ #
+ # @param modules [Set, Enumerable] the object to ensure becomes a frozen Set of Modules
+ # @return [Set] frozen Set containing the given modules
+ # @raise [ArgumentError] when the modules param is not an Enumerable
+ # @raise [Schema::NotASchemaError] when the modules param contains objects which are not Schemas
+ def ensure_module_set(modules)
+ if modules.is_a?(Set) && modules.frozen?
+ set = modules
+ else
+ set = Set.new(modules).freeze
+ end
+ not_modules = set.reject { |s| s.is_a?(Module) }
+ if !not_modules.empty?
+ raise(TypeError, [
+ "ensure_module_set given non-Module objects:",
+ *not_modules.map { |ns| ns.pretty_inspect.chomp },
+ ].join("\n"))
+ end
+
+ set
+ end
+
+ # is the given name ok to use as a ruby method name?
+ def ok_ruby_method_name?(name)
+ # must be a string
+ return false unless name.respond_to?(:to_str)
+ # must not begin with a digit
+ return false if name =~ /\A[0-9]/
+ # must not contain characters special to ruby syntax
+ return false if name =~ /[\\\s\#;\.,\(\)\[\]\{\}'"`%\+\-\/\*\^\|&=<>\?:!@\$~]/
+
+ return true
+ end
+
# this is the Y-combinator, which allows anonymous recursive functions. for a simple example,
# to define a recursive function to return the length of an array:
#
- # length = ycomb do |len|
- # proc { |list| list == [] ? 0 : 1 + len.call(list[1..-1]) }
- # end
+ # length = ycomb do |len|
+ # proc { |list| list == [] ? 0 : 1 + len.call(list[1..-1]) }
+ # end
#
- # see https://secure.wikimedia.org/wikipedia/en/wiki/Fixed_point_combinator#Y_combinator
- # and chapter 9 of the little schemer, available as the sample chapter at http://www.ccs.neu.edu/home/matthias/BTLS/
+ # length.call([0])
+ # # => 1
+ #
+ # see https://en.wikipedia.org/wiki/Fixed-point_combinator#Y_combinator
+ # and chapter 9 of the little schemer, available as the sample chapter at
+ # https://felleisen.org/matthias/BTLS-index.html
def ycomb
proc { |f| f.call(f) }.call(proc { |f| yield proc { |*x| f.call(f).call(*x) } })
end
- module_function :ycomb
module FingerprintHash
# overrides BasicObject#==
def ==(other)
- object_id == other.object_id || (other.respond_to?(:jsi_fingerprint) && other.jsi_fingerprint == self.jsi_fingerprint)
+ __id__ == other.__id__ || (other.respond_to?(:jsi_fingerprint) && other.jsi_fingerprint == self.jsi_fingerprint)
end
alias_method :eql?, :==
# overrides Kernel#hash
def hash
jsi_fingerprint.hash
end
end
- module Memoize
- def jsi_memoize(key, *args_)
- @jsi_memos ||= {}
- @jsi_memos[key] ||= Hash.new do |h, args|
- h[args] = yield(*args)
- end
- @jsi_memos[key][args_]
+ class MemoMap
+ Result = Util::AttrStruct[*%w(
+ value
+ inputs
+ inputs_hash
+ )]
+
+ class Result
end
- def jsi_clear_memo(key, *args)
- @jsi_memos ||= {}
- if @jsi_memos[key]
- if args.empty?
- @jsi_memos[key].clear
+ def initialize(key_by: nil, &block)
+ @key_by = key_by
+ @block = block
+
+ # each result has its own mutex to update its memoized value thread-safely
+ @result_mutexes = {}
+ # another mutex to thread-safely initialize each result mutex
+ @result_mutexes_mutex = Mutex.new
+
+ @results = {}
+ end
+
+ def [](*inputs)
+ if @key_by
+ key = @key_by.call(*inputs)
+ else
+ key = inputs
+ end
+ result_mutex = @result_mutexes_mutex.synchronize do
+ @result_mutexes[key] ||= Mutex.new
+ end
+
+ result_mutex.synchronize do
+ inputs_hash = inputs.hash
+ if @results.key?(key) && inputs_hash == @results[key].inputs_hash && inputs == @results[key].inputs
+ @results[key].value
else
- @jsi_memos[key].delete(args)
+ value = @block.call(*inputs)
+ @results[key] = Result.new(value: value, inputs: inputs, inputs_hash: inputs_hash)
+ value
end
end
end
end
+
+ module Memoize
+ def self.extended(object)
+ object.send(:jsi_initialize_memos)
+ end
+
+ private
+
+ def jsi_initialize_memos
+ @jsi_memomaps_mutex = Mutex.new
+ @jsi_memomaps = {}
+ end
+
+ # @return [Util::MemoMap]
+ def jsi_memomap(name, **options, &block)
+ raise(Bug, 'must jsi_initialize_memos') unless @jsi_memomaps
+ unless @jsi_memomaps.key?(name)
+ @jsi_memomaps_mutex.synchronize do
+ # note: this ||= appears redundant with `unless @jsi_memomaps.key?(name)`,
+ # but that check is not thread safe. this check is.
+ @jsi_memomaps[name] ||= Util::MemoMap.new(**options, &block)
+ end
+ end
+ @jsi_memomaps[name]
+ end
+
+ def jsi_memoize(name, *inputs, &block)
+ jsi_memomap(name, &block)[*inputs]
+ end
+ end
+
+ module Virtual
+ class InstantiationError < StandardError
+ end
+
+ # this virtual class is not intended to be instantiated except by its subclasses, which override #initialize
+ def initialize
+ # :nocov:
+ raise(InstantiationError, "cannot instantiate virtual class #{self.class}")
+ # :nocov:
+ end
+
+ # virtual_method is used to indicate that the method calling it must be implemented on the (non-virtual) subclass
+ def virtual_method
+ # :nocov:
+ raise(Bug, "class #{self.class} must implement #{caller_locations.first.label}")
+ # :nocov:
+ end
+ end
+
+ public
+
+ extend self
end
public
extend Util
end