File: cloner.rb

package info (click to toggle)
ruby-backports 3.25.3-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 4,912 kB
  • sloc: ruby: 11,759; makefile: 6
file content (103 lines) | stat: -rw-r--r-- 2,745 bytes parent folder | download | duplicates (2)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
# shareable_constant_value: literal

using ::RubyNext if defined?(::RubyNext)

module Backports
  class Ractor
    class Cloner
      class << self
        def deep_clone(obj)
          return obj if Ractor.ractor_shareable_self?(obj, false) { false }

          new.deep_clone(obj)
        end

        private :new
      end

      def initialize
        @processed = {}.compare_by_identity
        @changed = nil
      end

      def deep_clone(obj)
        result = process(obj) do |r|
          copy_contents(r)
        end
        return result if result

        Ractor.ractor_mark_set_shareable(@processed)
        obj
      end

      # Yields a deep copy.
      # If no deep copy is needed, `obj` is returned and
      # nothing is yielded
      private def clone_deeper(obj)
        return obj if Ractor.ractor_shareable_self?(obj, false) { false }

        result = process(obj) do |r|
          copy_contents(r)
        end
        return obj unless result

        yield result if block_given?
        result
      end

      # Yields if `obj` is a new structure
      # Returns the deep copy, or `false` if no deep copy is needed
      private def process(obj)
        @processed.fetch(obj) do
          # For recursive structures, assume that we'll need a duplicate.
          # If that's not the case, we will have duplicated the whole structure
          # for nothing...
          @processed[obj] = result = obj.dup
          changed = track_change { yield result }
          return false if obj.frozen? && !changed

          @changed = true
          result.freeze if obj.frozen?

          result
        end
      end

      # returns if the block called `deep clone` and that the deep copy was needed
      private def track_change
        prev = @changed
        @changed = false
        yield
        @changed
      ensure
        @changed = prev
      end

      # modifies in place `obj` by calling `deep clone` on its contents
      private def copy_contents(obj)
        case obj
        when ::Hash
          if obj.default
            clone_deeper(obj.default) do |copy|
              obj.default = copy
            end
          end
          obj.transform_keys! { |key| clone_deeper(key) }
          obj.transform_values! { |value| clone_deeper(value) }
        when ::Array
          obj.map! { |item| clone_deeper(item) }
        when ::Struct
          obj.each_pair do |key, item|
            clone_deeper(item) { |copy| obj[key] = copy }
          end
        end
        obj.instance_variables.each do |var|
          clone_deeper(obj.instance_variable_get(var)) do |copy|
            obj.instance_variable_set(var, copy)
          end
        end
      end
    end
    private_constant :Cloner
  end
end