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