farce 0 → 0.1.0
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 +4 -4
- data/CHANGELOG.md +9 -0
- data/CODE_OF_CONDUCT.md +26 -0
- data/CONTRIBUTING.md +71 -0
- data/MIT-LICENSE +20 -0
- data/README.md +1525 -0
- data/SECURITY.md +10 -0
- data/docs/benchmarks.md +185 -0
- data/docs/gems/dry-types.md +290 -0
- data/docs/gems/msgpack.md +70 -0
- data/docs/gems/ractor-shim.md +63 -0
- data/docs/modes.md +628 -0
- data/docs/scopes.md +649 -0
- data/docs/variants.md +346 -0
- data/ext/ext_helper.rb +20 -0
- data/ext/farce/README.md +21 -0
- data/ext/farce/atom.c +1023 -0
- data/ext/farce/bounded_map.c +1683 -0
- data/ext/farce/containers.h +77 -0
- data/ext/farce/counter.c +399 -0
- data/ext/farce/darwin.c +100 -0
- data/ext/farce/depend +12 -0
- data/ext/farce/dict.c +1523 -0
- data/ext/farce/dict.h +152 -0
- data/ext/farce/drivers.c +237 -0
- data/ext/farce/exchanger.c +299 -0
- data/ext/farce/extconf.rb +99 -0
- data/ext/farce/farce.c +359 -0
- data/ext/farce/flag.c +288 -0
- data/ext/farce/io.c +338 -0
- data/ext/farce/lock.c +510 -0
- data/ext/farce/map.c +2240 -0
- data/ext/farce/priority_queue.c +2056 -0
- data/ext/farce/queue.c +1059 -0
- data/ext/farce/reactor.c +820 -0
- data/ext/farce/reactor.h +103 -0
- data/ext/farce/shareable.h +31 -0
- data/ext/farce/signal.c +350 -0
- data/ext/farce/transaction.c +354 -0
- data/ext/farce/transaction.h +40 -0
- data/ext/farce/tree_map.c +1953 -0
- data/ext/farce/trie.c +2020 -0
- data/ext/farce/unshareable.c +155 -0
- data/ext/farce/unshared_io_pool.h +234 -0
- data/ext/farce/unshared_signal.c +263 -0
- data/ext/farce/unshared_wait.h +193 -0
- data/ext/farce/unsupported.c +6 -0
- data/ext/farce/vector.c +1195 -0
- data/ext/farce/weak_map.c +1714 -0
- data/ext/java/org/farce/BoundedMap.java +394 -0
- data/ext/java/org/farce/FiberScheduler.java +141 -0
- data/ext/java/org/farce/PriorityKey.java +45 -0
- data/ext/java/org/farce/PriorityQueue.java +373 -0
- data/ext/java/org/farce/QueueSignal.java +28 -0
- data/ext/rebind/README.md +14 -0
- data/ext/rebind/extconf.rb +10 -0
- data/ext/rebind/rebind.c +186 -0
- data/lib/farce/_yard/internal.rb +13 -0
- data/lib/farce/_yard/macros.rb +61 -0
- data/lib/farce/_yard/ractor.rb +46 -0
- data/lib/farce/abstract/atom.rb +186 -0
- data/lib/farce/abstract/bounded_map.rb +232 -0
- data/lib/farce/abstract/collection.rb +151 -0
- data/lib/farce/abstract/concurrent_map.rb +381 -0
- data/lib/farce/abstract/counter.rb +193 -0
- data/lib/farce/abstract/duplicable_map.rb +229 -0
- data/lib/farce/abstract/exchanger.rb +26 -0
- data/lib/farce/abstract/flag.rb +71 -0
- data/lib/farce/abstract/lazy.rb +115 -0
- data/lib/farce/abstract/lease.rb +104 -0
- data/lib/farce/abstract/lease_map.rb +261 -0
- data/lib/farce/abstract/lease_pool.rb +91 -0
- data/lib/farce/abstract/lfu_map.rb +20 -0
- data/lib/farce/abstract/lru_map.rb +25 -0
- data/lib/farce/abstract/map.rb +345 -0
- data/lib/farce/abstract/molecule.rb +245 -0
- data/lib/farce/abstract/port.rb +74 -0
- data/lib/farce/abstract/priority_queue.rb +117 -0
- data/lib/farce/abstract/queue.rb +269 -0
- data/lib/farce/abstract/scheduler.rb +111 -0
- data/lib/farce/abstract/set.rb +910 -0
- data/lib/farce/abstract/sorted_set.rb +172 -0
- data/lib/farce/abstract/timer_queue.rb +136 -0
- data/lib/farce/abstract/tree_map.rb +269 -0
- data/lib/farce/abstract/value.rb +68 -0
- data/lib/farce/abstract/vector.rb +1126 -0
- data/lib/farce/abstract/weak_atom.rb +32 -0
- data/lib/farce/abstract/weak_key_map.rb +12 -0
- data/lib/farce/abstract/weak_map.rb +13 -0
- data/lib/farce/abstract/weak_set.rb +12 -0
- data/lib/farce/abstract/weak_value_map.rb +12 -0
- data/lib/farce/abstract.rb +17 -0
- data/lib/farce/atom.rb +293 -0
- data/lib/farce/class_mirror.rb +87 -0
- data/lib/farce/clock.rb +121 -0
- data/lib/farce/config.rb +229 -0
- data/lib/farce/counter.rb +71 -0
- data/lib/farce/deduper.rb +122 -0
- data/lib/farce/engine/jruby/bounded_map.rb +314 -0
- data/lib/farce/engine/jruby/fiber_scheduler.jar +0 -0
- data/lib/farce/engine/jruby/fiber_scheduler.rb +119 -0
- data/lib/farce/engine/jruby/lease_waiting.rb +19 -0
- data/lib/farce/engine/jruby/map.rb +505 -0
- data/lib/farce/engine/jruby/mutable_numeric_copy.rb +42 -0
- data/lib/farce/engine/jruby/signal.rb +147 -0
- data/lib/farce/engine/jruby.rb +63 -0
- data/lib/farce/engine/jvm/concurrent_weak_registry.rb +55 -0
- data/lib/farce/engine/jvm/counter.rb +102 -0
- data/lib/farce/engine/jvm/extension.rb +32 -0
- data/lib/farce/engine/jvm/farce.jar +0 -0
- data/lib/farce/engine/jvm/flag.rb +79 -0
- data/lib/farce/engine/jvm/priority_queue.rb +217 -0
- data/lib/farce/engine/jvm/tree_map.rb +350 -0
- data/lib/farce/engine/jvm/types.rb +180 -0
- data/lib/farce/engine/jvm.rb +19 -0
- data/lib/farce/engine/ruby/3.4/fiber_scheduler.rb +20 -0
- data/lib/farce/engine/ruby/3.4/port.rb +186 -0
- data/lib/farce/engine/ruby/3.4/ractor_methods.rb +26 -0
- data/lib/farce/engine/ruby/3.4/ractor_selector.rb +92 -0
- data/lib/farce/engine/ruby/3.4/vault.rb +56 -0
- data/lib/farce/engine/ruby/4.0/port.rb +19 -0
- data/lib/farce/engine/ruby/4.0/ractor_methods.rb +20 -0
- data/lib/farce/engine/ruby/4.0/ractor_selector.rb +108 -0
- data/lib/farce/engine/ruby/4.0/vault.rb +74 -0
- data/lib/farce/engine/ruby/4.1/port.rb +21 -0
- data/lib/farce/engine/ruby/4.1/ractor_methods.rb +22 -0
- data/lib/farce/engine/ruby/4.1/ractor_selector.rb +25 -0
- data/lib/farce/engine/ruby/4.1/vault.rb +5 -0
- data/lib/farce/engine/ruby/fiber_scheduler.rb +32 -0
- data/lib/farce/engine/ruby/key_lock_map.rb +28 -0
- data/lib/farce/engine/ruby/shared/lease.rb +26 -0
- data/lib/farce/engine/ruby/shared/lease_pool.rb +24 -0
- data/lib/farce/engine/ruby/shared/main_scheduler.rb +9 -0
- data/lib/farce/engine/ruby/shared/parallel_scheduler.rb +9 -0
- data/lib/farce/engine/ruby/shared/proxy_owner.rb +31 -0
- data/lib/farce/engine/ruby/shared/ractor_methods.rb +34 -0
- data/lib/farce/engine/ruby/shared/ractor_selector.rb +328 -0
- data/lib/farce/engine/ruby/shared/strict_map.rb +13 -0
- data/lib/farce/engine/ruby/shared/unshared_vector.rb +14 -0
- data/lib/farce/engine/ruby/shared/vault.rb +144 -0
- data/lib/farce/engine/ruby/shared/vault_weak_map.rb +336 -0
- data/lib/farce/engine/ruby/shared/weak_atom.rb +54 -0
- data/lib/farce/engine/ruby/shared/weak_map.rb +413 -0
- data/lib/farce/engine/ruby.rb +130 -0
- data/lib/farce/engine/shared/atom.rb +10 -0
- data/lib/farce/engine/shared/exchanger.rb +130 -0
- data/lib/farce/engine/shared/identity_key.rb +22 -0
- data/lib/farce/engine/shared/lease.rb +10 -0
- data/lib/farce/engine/shared/lease_pool.rb +10 -0
- data/lib/farce/engine/shared/main_scheduler.rb +20 -0
- data/lib/farce/engine/shared/map_key_coordination.rb +226 -0
- data/lib/farce/engine/shared/parallel_scheduler.rb +9 -0
- data/lib/farce/engine/shared/port.rb +44 -0
- data/lib/farce/engine/shared/portable_bounded_map.rb +619 -0
- data/lib/farce/engine/shared/proxy_owner.rb +43 -0
- data/lib/farce/engine/shared/queue.rb +233 -0
- data/lib/farce/engine/shared/ractor_methods.rb +73 -0
- data/lib/farce/engine/shared/rebindable.rb +15 -0
- data/lib/farce/engine/shared/strict_atom.rb +73 -0
- data/lib/farce/engine/shared/strict_map.rb +117 -0
- data/lib/farce/engine/shared/strict_queue_values.rb +27 -0
- data/lib/farce/engine/shared/strict_tree_map.rb +56 -0
- data/lib/farce/engine/shared/transaction_map_backend.rb +20 -0
- data/lib/farce/engine/shared/trie.rb +317 -0
- data/lib/farce/engine/shared/trie_builder.rb +199 -0
- data/lib/farce/engine/shared/unshareable.rb +14 -0
- data/lib/farce/engine/shared/unshared_atom.rb +256 -0
- data/lib/farce/engine/shared/unshared_priority_queue.rb +9 -0
- data/lib/farce/engine/shared/unshared_queue.rb +18 -0
- data/lib/farce/engine/shared/unshared_signal.rb +21 -0
- data/lib/farce/engine/shared/unshared_vector.rb +377 -0
- data/lib/farce/engine/shared/unshared_weak_atom.rb +30 -0
- data/lib/farce/engine/shared/unshared_weak_map.rb +445 -0
- data/lib/farce/engine/shared/vault.rb +31 -0
- data/lib/farce/engine/shared/vector.rb +83 -0
- data/lib/farce/engine/shared/weak_atom/base.rb +189 -0
- data/lib/farce/engine/shared/weak_atom.rb +20 -0
- data/lib/farce/engine/shared/weak_map/cell.rb +99 -0
- data/lib/farce/engine/shared/weak_map/index.rb +249 -0
- data/lib/farce/engine/shared/weak_map/lock.rb +80 -0
- data/lib/farce/engine/shared/weak_map/reference.rb +33 -0
- data/lib/farce/engine/shared.rb +63 -0
- data/lib/farce/engine/truffleruby/fiber_scheduler.rb +13 -0
- data/lib/farce/engine/truffleruby/lock.rb +121 -0
- data/lib/farce/engine/truffleruby/map.rb +656 -0
- data/lib/farce/engine/truffleruby/native/counter.rb +105 -0
- data/lib/farce/engine/truffleruby/native/flag.rb +77 -0
- data/lib/farce/engine/truffleruby/native/ordered_array_support.rb +67 -0
- data/lib/farce/engine/truffleruby/native/priority_queue.rb +388 -0
- data/lib/farce/engine/truffleruby/native/tree_map.rb +51 -0
- data/lib/farce/engine/truffleruby/native/unsafe_tree_map.rb +350 -0
- data/lib/farce/engine/truffleruby/signal.rb +77 -0
- data/lib/farce/engine/truffleruby.rb +148 -0
- data/lib/farce/envelope.rb +335 -0
- data/lib/farce/error.rb +35 -0
- data/lib/farce/exchanger.rb +41 -0
- data/lib/farce/flag.rb +27 -0
- data/lib/farce/integrations/active_support/blank.rb +68 -0
- data/lib/farce/integrations/active_support/clock.rb +17 -0
- data/lib/farce/integrations/active_support/duplicable.rb +78 -0
- data/lib/farce/integrations/active_support/map.rb +175 -0
- data/lib/farce/integrations/active_support/set.rb +60 -0
- data/lib/farce/integrations/active_support/value_serialization.rb +17 -0
- data/lib/farce/integrations/active_support/vector.rb +231 -0
- data/lib/farce/integrations/active_support.rb +9 -0
- data/lib/farce/integrations/activesupport.rb +5 -0
- data/lib/farce/integrations/bson.rb +108 -0
- data/lib/farce/integrations/cbor.rb +51 -0
- data/lib/farce/integrations/concurrent.rb +112 -0
- data/lib/farce/integrations/dry-types.rb +5 -0
- data/lib/farce/integrations/dry_types.rb +694 -0
- data/lib/farce/integrations/json.rb +7 -0
- data/lib/farce/integrations/msgpack.rb +112 -0
- data/lib/farce/integrations/oj.rb +69 -0
- data/lib/farce/integrations/psych.rb +191 -0
- data/lib/farce/integrations/ractor-sharing.rb +5 -0
- data/lib/farce/integrations/ractor-tmvar.rb +5 -0
- data/lib/farce/integrations/ractor_sharing.rb +172 -0
- data/lib/farce/integrations/ractor_tmvar.rb +64 -0
- data/lib/farce/integrations/shared/to_json.rb +42 -0
- data/lib/farce/integrations/sorted_set.rb +11 -0
- data/lib/farce/integrations/weakref.rb +38 -0
- data/lib/farce/integrations/yajl.rb +10 -0
- data/lib/farce/integrations.rb +156 -0
- data/lib/farce/internal/_frozen_config.rb +11 -0
- data/lib/farce/internal/autoloads.rb +47 -0
- data/lib/farce/internal/blocking_priority_queue.rb +122 -0
- data/lib/farce/internal/converter.rb +106 -0
- data/lib/farce/internal/copyable.rb +31 -0
- data/lib/farce/internal/delegation.rb +20 -0
- data/lib/farce/internal/external_transaction.rb +59 -0
- data/lib/farce/internal/fake_ractor.rb +179 -0
- data/lib/farce/internal/freeze.rb +118 -0
- data/lib/farce/internal/inspect.rb +157 -0
- data/lib/farce/internal/key_lock_map.rb +29 -0
- data/lib/farce/internal/key_normalizer.rb +289 -0
- data/lib/farce/internal/lease_initialization.rb +115 -0
- data/lib/farce/internal/lease_map.rb +441 -0
- data/lib/farce/internal/lease_pool_state.rb +228 -0
- data/lib/farce/internal/lease_state.rb +342 -0
- data/lib/farce/internal/lease_waiting.rb +13 -0
- data/lib/farce/internal/managed_queue.rb +26 -0
- data/lib/farce/internal/map_value_modes.rb +227 -0
- data/lib/farce/internal/marshal_support.rb +227 -0
- data/lib/farce/internal/mixin.rb +20 -0
- data/lib/farce/internal/mutable_ordered_key_lock_map.rb +63 -0
- data/lib/farce/internal/noncopyable.rb +17 -0
- data/lib/farce/internal/ordered_key_lock_map.rb +69 -0
- data/lib/farce/internal/pool_supervisor.rb +41 -0
- data/lib/farce/internal/pool_worker.rb +61 -0
- data/lib/farce/internal/portable_transaction/reservation_entry.rb +29 -0
- data/lib/farce/internal/portable_transaction/strong_map_entry.rb +137 -0
- data/lib/farce/internal/portable_transaction/strong_map_size_entry.rb +44 -0
- data/lib/farce/internal/portable_transaction/tree_entry.rb +64 -0
- data/lib/farce/internal/portable_transaction.rb +164 -0
- data/lib/farce/internal/proxy_owner_notifications.rb +29 -0
- data/lib/farce/internal/reservation_waiting.rb +24 -0
- data/lib/farce/internal/scheduled_task.rb +48 -0
- data/lib/farce/internal/scheduler_io.rb +171 -0
- data/lib/farce/internal/scheduler_lifecycle.rb +314 -0
- data/lib/farce/internal/select_scheduler.rb +235 -0
- data/lib/farce/internal/storage.rb +162 -0
- data/lib/farce/internal/strict_lease.rb +30 -0
- data/lib/farce/internal/strict_lease_map.rb +20 -0
- data/lib/farce/internal/strict_lease_pool.rb +29 -0
- data/lib/farce/internal/thread_pool.rb +166 -0
- data/lib/farce/internal/transaction_conflict.rb +9 -0
- data/lib/farce/internal/transaction_freeze_guard.rb +30 -0
- data/lib/farce/internal/transaction_map_snapshot.rb +123 -0
- data/lib/farce/internal/undefined.rb +22 -0
- data/lib/farce/internal/unshared_lease.rb +29 -0
- data/lib/farce/internal/unshared_lease_pool.rb +34 -0
- data/lib/farce/internal/unshared_queue_waiting.rb +28 -0
- data/lib/farce/internal/value_serialization.rb +13 -0
- data/lib/farce/internal/weak_map_value_modes.rb +58 -0
- data/lib/farce/internal/weak_mode_manager.rb +26 -0
- data/lib/farce/internal.rb +129 -0
- data/lib/farce/lazy.rb +100 -0
- data/lib/farce/lazy_ref.rb +40 -0
- data/lib/farce/lease.rb +28 -0
- data/lib/farce/lease_map.rb +34 -0
- data/lib/farce/lease_pool.rb +29 -0
- data/lib/farce/lfu_map.rb +53 -0
- data/lib/farce/local/atom.rb +23 -0
- data/lib/farce/local/counter.rb +88 -0
- data/lib/farce/local/flag.rb +65 -0
- data/lib/farce/local/lazy.rb +38 -0
- data/lib/farce/local/lazy_ref.rb +36 -0
- data/lib/farce/local/lease.rb +77 -0
- data/lib/farce/local/lease_map.rb +80 -0
- data/lib/farce/local/lease_pool.rb +38 -0
- data/lib/farce/local/lfu_map.rb +55 -0
- data/lib/farce/local/lru_map.rb +62 -0
- data/lib/farce/local/map.rb +47 -0
- data/lib/farce/local/molecule.rb +45 -0
- data/lib/farce/local/priority_queue.rb +53 -0
- data/lib/farce/local/queue.rb +32 -0
- data/lib/farce/local/scoped.rb +166 -0
- data/lib/farce/local/set.rb +18 -0
- data/lib/farce/local/sorted_set.rb +50 -0
- data/lib/farce/local/timer_queue.rb +41 -0
- data/lib/farce/local/tree_map.rb +47 -0
- data/lib/farce/local/vector.rb +30 -0
- data/lib/farce/local/weak_atom.rb +23 -0
- data/lib/farce/local/weak_key_map.rb +39 -0
- data/lib/farce/local/weak_map.rb +39 -0
- data/lib/farce/local/weak_set.rb +18 -0
- data/lib/farce/local/weak_value_map.rb +39 -0
- data/lib/farce/local.rb +35 -0
- data/lib/farce/lock.rb +54 -0
- data/lib/farce/lru_map.rb +63 -0
- data/lib/farce/map.rb +58 -0
- data/lib/farce/mode_manager.rb +142 -0
- data/lib/farce/molecule.rb +60 -0
- data/lib/farce/mutable.rb +171 -0
- data/lib/farce/pool.rb +332 -0
- data/lib/farce/port.rb +134 -0
- data/lib/farce/priority_queue.rb +70 -0
- data/lib/farce/proxy/register.rb +101 -0
- data/lib/farce/proxy/supervisor.rb +137 -0
- data/lib/farce/proxy/wrapper.rb +49 -0
- data/lib/farce/proxy.rb +291 -0
- data/lib/farce/queue.rb +60 -0
- data/lib/farce/ractor.rb +269 -0
- data/lib/farce/read_write_lock.rb +256 -0
- data/lib/farce/reference.rb +158 -0
- data/lib/farce/resolv/dns.rb +42 -0
- data/lib/farce/resolv.rb +85 -0
- data/lib/farce/scheduler.rb +536 -0
- data/lib/farce/set.rb +39 -0
- data/lib/farce/shareable.rb +150 -0
- data/lib/farce/signal.rb +97 -0
- data/lib/farce/sorted_set.rb +36 -0
- data/lib/farce/strict/atom.rb +34 -0
- data/lib/farce/strict/counter.rb +10 -0
- data/lib/farce/strict/exchanger.rb +25 -0
- data/lib/farce/strict/flag.rb +10 -0
- data/lib/farce/strict/lazy.rb +26 -0
- data/lib/farce/strict/lazy_ref.rb +26 -0
- data/lib/farce/strict/lease.rb +27 -0
- data/lib/farce/strict/lease_map.rb +33 -0
- data/lib/farce/strict/lease_pool.rb +28 -0
- data/lib/farce/strict/lfu_map.rb +27 -0
- data/lib/farce/strict/lru_map.rb +28 -0
- data/lib/farce/strict/map.rb +54 -0
- data/lib/farce/strict/molecule.rb +16 -0
- data/lib/farce/strict/port.rb +40 -0
- data/lib/farce/strict/priority_queue.rb +20 -0
- data/lib/farce/strict/queue.rb +17 -0
- data/lib/farce/strict/set.rb +14 -0
- data/lib/farce/strict/sorted_set.rb +19 -0
- data/lib/farce/strict/timer_queue.rb +13 -0
- data/lib/farce/strict/tree_map.rb +27 -0
- data/lib/farce/strict/vector.rb +24 -0
- data/lib/farce/strict/weak_atom.rb +31 -0
- data/lib/farce/strict/weak_key_map.rb +49 -0
- data/lib/farce/strict/weak_map.rb +49 -0
- data/lib/farce/strict/weak_set.rb +14 -0
- data/lib/farce/strict/weak_value_map.rb +48 -0
- data/lib/farce/strict.rb +28 -0
- data/lib/farce/system.rb +40 -0
- data/lib/farce/thread_scheduler.rb +70 -0
- data/lib/farce/timer_queue.rb +66 -0
- data/lib/farce/transaction/atom.rb +73 -0
- data/lib/farce/transaction/map.rb +25 -0
- data/lib/farce/transaction/map_operations.rb +143 -0
- data/lib/farce/transaction/molecule.rb +90 -0
- data/lib/farce/transaction/mutable.rb +61 -0
- data/lib/farce/transaction/set.rb +17 -0
- data/lib/farce/transaction/set_operations.rb +48 -0
- data/lib/farce/transaction/sorted_set.rb +18 -0
- data/lib/farce/transaction/tree_map.rb +78 -0
- data/lib/farce/transaction/vector.rb +139 -0
- data/lib/farce/transaction/wrapper.rb +202 -0
- data/lib/farce/transaction.rb +343 -0
- data/lib/farce/tree_map.rb +56 -0
- data/lib/farce/unsafe/lfu_map.rb +36 -0
- data/lib/farce/unsafe/lru_map.rb +36 -0
- data/lib/farce/unsafe/tree_map.rb +21 -0
- data/lib/farce/unsafe.rb +29 -0
- data/lib/farce/unshareable.rb +67 -0
- data/lib/farce/unshared/atom.rb +24 -0
- data/lib/farce/unshared/counter.rb +10 -0
- data/lib/farce/unshared/flag.rb +10 -0
- data/lib/farce/unshared/lazy.rb +38 -0
- data/lib/farce/unshared/lazy_ref.rb +26 -0
- data/lib/farce/unshared/lease.rb +27 -0
- data/lib/farce/unshared/lease_map.rb +26 -0
- data/lib/farce/unshared/lease_pool.rb +25 -0
- data/lib/farce/unshared/lfu_map.rb +20 -0
- data/lib/farce/unshared/lru_map.rb +20 -0
- data/lib/farce/unshared/map.rb +49 -0
- data/lib/farce/unshared/molecule.rb +16 -0
- data/lib/farce/unshared/priority_queue.rb +16 -0
- data/lib/farce/unshared/queue.rb +27 -0
- data/lib/farce/unshared/set.rb +14 -0
- data/lib/farce/unshared/sorted_set.rb +28 -0
- data/lib/farce/unshared/timer_queue.rb +16 -0
- data/lib/farce/unshared/tree_map.rb +21 -0
- data/lib/farce/unshared/vector.rb +19 -0
- data/lib/farce/unshared/weak_atom.rb +31 -0
- data/lib/farce/unshared/weak_key_map.rb +44 -0
- data/lib/farce/unshared/weak_map.rb +44 -0
- data/lib/farce/unshared/weak_set.rb +14 -0
- data/lib/farce/unshared/weak_value_map.rb +43 -0
- data/lib/farce/unshared.rb +28 -0
- data/lib/farce/vector.rb +228 -0
- data/lib/farce/version.rb +8 -0
- data/lib/farce/walker/definitions.rb +186 -0
- data/lib/farce/walker/modification.rb +355 -0
- data/lib/farce/walker.rb +319 -0
- data/lib/farce/weak_atom.rb +121 -0
- data/lib/farce/weak_key_map.rb +31 -0
- data/lib/farce/weak_map.rb +33 -0
- data/lib/farce/weak_ref.rb +67 -0
- data/lib/farce/weak_set.rb +82 -0
- data/lib/farce/weak_value.rb +195 -0
- data/lib/farce/weak_value_map.rb +32 -0
- data/lib/farce.rb +519 -0
- metadata +435 -8
|
@@ -0,0 +1,355 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
# shareable_constant_value: literal
|
|
3
|
+
# warn_indent: true
|
|
4
|
+
|
|
5
|
+
module Farce
|
|
6
|
+
class Walker
|
|
7
|
+
# @api private
|
|
8
|
+
# Ignore the man behind the curtain!
|
|
9
|
+
#
|
|
10
|
+
# This is a helper class for lazy-modification that is also cycle-aware.
|
|
11
|
+
class Modification < Walker
|
|
12
|
+
public_class_method :new
|
|
13
|
+
|
|
14
|
+
# Bound callback replay rounds for cycles that do not converge, regardless of graph depth.
|
|
15
|
+
MAX_CYCLE_REPLAYS = 32
|
|
16
|
+
|
|
17
|
+
# Track an object's replacements, pending writes, and cycle discovery state.
|
|
18
|
+
Node = Struct.new(:original, :result, :destination, :index, :low, :active,
|
|
19
|
+
:plans, :changed, :cyclic, :freeze_result, :finalizer, :skipped, :callback)
|
|
20
|
+
|
|
21
|
+
# Record child references and the writer that installs their resolved values.
|
|
22
|
+
Plan = Struct.new(:children, :results, :writer, :hash_keys, :dirty, :applied, :target, :key_stride)
|
|
23
|
+
private_constant :Node, :Plan, :MAX_CYCLE_REPLAYS
|
|
24
|
+
|
|
25
|
+
# Keep identity and cycle tracking state local to this walk.
|
|
26
|
+
def initialize(...)
|
|
27
|
+
super
|
|
28
|
+
@nodes = {}.compare_by_identity
|
|
29
|
+
@stack = []
|
|
30
|
+
@sequence = 0
|
|
31
|
+
@node = nil
|
|
32
|
+
@replaying = false
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Reuse known results and settle each connected cycle once all its nodes are visited.
|
|
36
|
+
def visit(object, &callback)
|
|
37
|
+
parent = @node
|
|
38
|
+
previous_object = @current_object
|
|
39
|
+
if node = @nodes[object]
|
|
40
|
+
if node.active && parent
|
|
41
|
+
parent.low = node.index if parent.low > node.index
|
|
42
|
+
parent.cyclic = node.cyclic = true
|
|
43
|
+
end
|
|
44
|
+
return node.result
|
|
45
|
+
end
|
|
46
|
+
return @seen[object] if @seen.key?(object)
|
|
47
|
+
|
|
48
|
+
return super unless Internal.garbage_collectable?(object)
|
|
49
|
+
|
|
50
|
+
index = @sequence
|
|
51
|
+
@sequence += 1
|
|
52
|
+
node = Node.new(object, object, object, index, index, true)
|
|
53
|
+
node.callback = callback
|
|
54
|
+
@nodes[object] = node
|
|
55
|
+
|
|
56
|
+
@stack << node
|
|
57
|
+
|
|
58
|
+
@node = node
|
|
59
|
+
@current_object = object
|
|
60
|
+
node.result = catch(SKIP) { (node.callback || @callback).call(object, self) }
|
|
61
|
+
node.changed ||= !node.result.equal?(object)
|
|
62
|
+
|
|
63
|
+
if node.low == node.index && !node.cyclic
|
|
64
|
+
@stack.pop
|
|
65
|
+
finish(node)
|
|
66
|
+
node.active = false
|
|
67
|
+
node.plans = nil
|
|
68
|
+
elsif node.low == node.index
|
|
69
|
+
start = @stack.index { it.equal?(node) }
|
|
70
|
+
component = @stack.slice!(start..)
|
|
71
|
+
settle(component)
|
|
72
|
+
component.each { it.active = false }
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
if parent && node.active
|
|
76
|
+
parent.low = node.low if parent.low > node.low
|
|
77
|
+
parent.cyclic = true
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
node.result
|
|
81
|
+
ensure
|
|
82
|
+
@node = parent
|
|
83
|
+
@current_object = previous_object
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# Traverse alternate targets normally and reuse prepared destinations during replay.
|
|
87
|
+
def traverse(object = current_object)
|
|
88
|
+
return visit(object) { |value| super(value) } if
|
|
89
|
+
@node && Internal.garbage_collectable?(object) && !object.equal?(@node.original) &&
|
|
90
|
+
!object.equal?(@node.destination)
|
|
91
|
+
return @node.destination if @replaying && object.equal?(@node.original)
|
|
92
|
+
super
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# Record child changes and defer writes until cyclic references can be resolved.
|
|
96
|
+
def update(object, values, hash_keys: false, key_stride: 1, &writer)
|
|
97
|
+
if hash_keys && !(Integer === key_stride && key_stride.positive?)
|
|
98
|
+
raise ArgumentError, "key_stride must be a positive Integer"
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
node = @node
|
|
102
|
+
unless object.equal?(node.original) || object.equal?(node.destination)
|
|
103
|
+
raise ArgumentError, "update requires the current traversal target"
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
results = values
|
|
107
|
+
dirty = false
|
|
108
|
+
|
|
109
|
+
values.each_with_index do |value, index|
|
|
110
|
+
result = visit(value)
|
|
111
|
+
child = @nodes[value]
|
|
112
|
+
node.changed ||= child&.changed
|
|
113
|
+
dirty ||= hash_keys && (index % key_stride).zero? && child && child.changed
|
|
114
|
+
|
|
115
|
+
next if value.equal?(result)
|
|
116
|
+
|
|
117
|
+
dirty = true
|
|
118
|
+
results = values.dup if results.equal?(values)
|
|
119
|
+
results[index] = result
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
node.changed ||= dirty
|
|
123
|
+
plan = Plan.new(values, results, writer, hash_keys, dirty, nil, nil, key_stride)
|
|
124
|
+
(node.plans ||= []) << plan
|
|
125
|
+
|
|
126
|
+
unless node.cyclic
|
|
127
|
+
prepare(node) if plan.dirty
|
|
128
|
+
apply(node, plan) if plan.dirty
|
|
129
|
+
end
|
|
130
|
+
node.destination
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
# Record Data members so changed values can initialize a replacement.
|
|
134
|
+
def rebuild_data(object)
|
|
135
|
+
members = object.class.members
|
|
136
|
+
values = members.map { object.__send__(it) }
|
|
137
|
+
update(object, values) do |target, results|
|
|
138
|
+
target.__send__(:initialize, **members.zip(results).to_h)
|
|
139
|
+
target
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
# Freeze the settled result. With copy enabled, preserve the input's state.
|
|
144
|
+
# Call this instead of freezing a preliminary result inside the callback.
|
|
145
|
+
def freeze_result
|
|
146
|
+
return send_to(current_object, :freeze) unless @node && @node.original.equal?(current_object)
|
|
147
|
+
@node.changed = true unless send_to(@node.original, :frozen?)
|
|
148
|
+
@node.freeze_result = true
|
|
149
|
+
prepare(@node) if @modify != true && !send_to(@node.destination, :frozen?)
|
|
150
|
+
@node.destination
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
# Run once after traversal and cycle resolution. The block receives the
|
|
154
|
+
# result and whether it belongs to a cycle. Cyclic finalizers must retain
|
|
155
|
+
# identity. Acyclic finalizers may return a canonical replacement.
|
|
156
|
+
def finalize(&block)
|
|
157
|
+
raise LocalJumpError, "no block given" unless block
|
|
158
|
+
return block.call(current_object, false) unless @node && @node.original.equal?(current_object)
|
|
159
|
+
@node.finalizer = block
|
|
160
|
+
@node.destination
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
# Exclude the current node from callback replay, freezing, and finalization.
|
|
164
|
+
def skip(object = current_object)
|
|
165
|
+
@node.skipped = true if @node && @node.original.equal?(current_object)
|
|
166
|
+
super
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
# Report whether the object or its visited descendants have changed.
|
|
170
|
+
def changed?(object = current_object)
|
|
171
|
+
node = @nodes[object]
|
|
172
|
+
node ? node.changed == true : (@seen.key?(object) && !@seen[object].equal?(object))
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
# Follow pending references to check that hashing cannot reach unfinished Data.
|
|
176
|
+
def hashable?(object)
|
|
177
|
+
return true unless @unfinished_data && !@unfinished_data.empty?
|
|
178
|
+
!Walker.visit(object, constants:, class_variables:) do |value, traversal|
|
|
179
|
+
traversal.return(true) if @unfinished_data.key?(value)
|
|
180
|
+
node = @nodes[value] || @destinations&.[](value)
|
|
181
|
+
if node && node.plans
|
|
182
|
+
traversal.return(true) if @unfinished_data.key?(node.destination)
|
|
183
|
+
node.plans.each { |plan| plan.children.each { traversal.visit(resolved(it)) } }
|
|
184
|
+
else
|
|
185
|
+
traversal.traverse
|
|
186
|
+
end
|
|
187
|
+
false
|
|
188
|
+
end
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
# Reject keys and set elements whose Data dependencies are still being built.
|
|
192
|
+
def check_hash_key(object)
|
|
193
|
+
return object if hashable?(object)
|
|
194
|
+
raise ArgumentError, "hash key or set element refers to an unfinished Data value"
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
private
|
|
198
|
+
|
|
199
|
+
# Look up the latest result for a recorded child reference.
|
|
200
|
+
def resolved(object)
|
|
201
|
+
node = @nodes[object]
|
|
202
|
+
node ? node.result : @seen.fetch(object, object)
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
# Refresh a pending write from current child results, including changed hash keys.
|
|
206
|
+
def refresh(node, plan)
|
|
207
|
+
dirty = false
|
|
208
|
+
plan.children.each_with_index do |value, index|
|
|
209
|
+
result = resolved(value)
|
|
210
|
+
unless plan.results[index].equal?(result)
|
|
211
|
+
plan.results = plan.children.dup if plan.results.equal?(plan.children)
|
|
212
|
+
plan.results[index] = result
|
|
213
|
+
end
|
|
214
|
+
child = @nodes[value]
|
|
215
|
+
node.changed ||= child&.changed
|
|
216
|
+
dirty ||= !value.equal?(result)
|
|
217
|
+
dirty ||= plan.hash_keys && (index % plan.key_stride).zero? && child && child.changed
|
|
218
|
+
end
|
|
219
|
+
node.changed ||= dirty
|
|
220
|
+
plan.dirty = dirty
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
# Choose a writable destination, copying lazily or allocating unfinished Data.
|
|
224
|
+
def prepare(node, rebuild: false)
|
|
225
|
+
previous = node.destination
|
|
226
|
+
return previous unless rebuild || previous.equal?(node.original)
|
|
227
|
+
object = node.original
|
|
228
|
+
return object if Internal::Noncopyable === object || Abstract::LeaseMap === object
|
|
229
|
+
destination =
|
|
230
|
+
if Data === object
|
|
231
|
+
value = object.class.allocate
|
|
232
|
+
(@unfinished_data ||= {}.compare_by_identity)[value] = true
|
|
233
|
+
value
|
|
234
|
+
elsif @modify == true
|
|
235
|
+
send_to(object, :frozen?) ? send_to(object, :clone, freeze: false) : object
|
|
236
|
+
elsif @modify == :clone
|
|
237
|
+
send_to(object, :clone, freeze: false)
|
|
238
|
+
else
|
|
239
|
+
send_to(object, @modify)
|
|
240
|
+
end
|
|
241
|
+
(@destinations ||= {}.compare_by_identity)[destination] = node unless destination.equal?(object)
|
|
242
|
+
node.destination = destination
|
|
243
|
+
node.result = destination if !node.skipped && (node.result.equal?(object) || node.result.equal?(previous))
|
|
244
|
+
destination
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
# Install resolved children and remember cyclic writes to avoid repeating them.
|
|
248
|
+
def apply(node, plan)
|
|
249
|
+
return if plan.target.equal?(node.destination) && plan.applied &&
|
|
250
|
+
plan.results.each_with_index.all? { |value, index| value.equal?(plan.applied[index]) }
|
|
251
|
+
if plan.children.equal?(node.destination)
|
|
252
|
+
children = plan.children.dup
|
|
253
|
+
plan.results = children if plan.results.equal?(plan.children)
|
|
254
|
+
plan.children = children
|
|
255
|
+
end
|
|
256
|
+
result = plan.writer.call(node.destination, plan.results)
|
|
257
|
+
if result && !result.equal?(node.destination)
|
|
258
|
+
old = node.destination
|
|
259
|
+
node.destination = result
|
|
260
|
+
node.result = result if node.result.equal?(old)
|
|
261
|
+
end
|
|
262
|
+
@unfinished_data&.delete(node.destination) if Data === node.destination
|
|
263
|
+
return unless node.cyclic
|
|
264
|
+
plan.target = node.destination
|
|
265
|
+
plan.applied = plan.results.dup
|
|
266
|
+
end
|
|
267
|
+
|
|
268
|
+
# Resolve a component and freeze all its results before running finalizers.
|
|
269
|
+
def settle(component)
|
|
270
|
+
settle_cycle(component) if component.size > 1 || component.first.cyclic
|
|
271
|
+
component.each { finish(it, finalize: false) }
|
|
272
|
+
component.each { finalize_node(it) } # rubocop:disable Style/CombinableLoops
|
|
273
|
+
end
|
|
274
|
+
|
|
275
|
+
# Apply the requested freezing policy and optionally finalize the result.
|
|
276
|
+
def finish(node, finalize: true)
|
|
277
|
+
return if node.skipped
|
|
278
|
+
@node = node
|
|
279
|
+
@current_object = node.original
|
|
280
|
+
freeze = node.freeze_result || @freeze == true || (@freeze.nil? && send_to(node.original, :frozen?))
|
|
281
|
+
if freeze
|
|
282
|
+
node.changed = true unless send_to(node.result, :frozen?)
|
|
283
|
+
prepare(node) if @modify != true && node.result.equal?(node.original) && !send_to(node.result, :frozen?)
|
|
284
|
+
send_to(node.result, :freeze)
|
|
285
|
+
end
|
|
286
|
+
finalize_node(node) if finalize
|
|
287
|
+
end
|
|
288
|
+
|
|
289
|
+
# Accept a final replacement while preserving the identity of cyclic results.
|
|
290
|
+
def finalize_node(node)
|
|
291
|
+
return if node.skipped || !node.finalizer
|
|
292
|
+
@node = node
|
|
293
|
+
@current_object = node.original
|
|
294
|
+
result = node.finalizer.call(node.result, node.cyclic == true)
|
|
295
|
+
raise ArgumentError, "a cyclic finalizer must preserve identity" if node.cyclic && !result.equal?(node.result)
|
|
296
|
+
node.changed ||= !result.equal?(node.result)
|
|
297
|
+
node.result = result
|
|
298
|
+
end
|
|
299
|
+
|
|
300
|
+
# Propagate replacements and replay callbacks until result identities stabilize.
|
|
301
|
+
def settle_cycle(component)
|
|
302
|
+
# Allocate destinations to a fixed point before wiring back references.
|
|
303
|
+
MAX_CYCLE_REPLAYS.times do
|
|
304
|
+
loop do
|
|
305
|
+
destinations = component.map(&:destination)
|
|
306
|
+
component.each do |node|
|
|
307
|
+
node.plans&.each { refresh(node, it) }
|
|
308
|
+
freezing = !node.skipped && (node.freeze_result || @freeze == true)
|
|
309
|
+
needed = node.plans&.any?(&:dirty) || (freezing && @modify != true && !send_to(node.original, :frozen?))
|
|
310
|
+
prepare(node) if needed
|
|
311
|
+
next unless Data === node.destination && send_to(node.destination, :frozen?) && node.plans&.any? do |plan|
|
|
312
|
+
plan.applied && plan.results.each_with_index.any? { |value, index| !value.equal?(plan.applied[index]) }
|
|
313
|
+
end
|
|
314
|
+
prepare(node, rebuild: true)
|
|
315
|
+
end
|
|
316
|
+
break if component.each_with_index.all? { |node, index| node.destination.equal?(destinations[index]) }
|
|
317
|
+
end
|
|
318
|
+
|
|
319
|
+
component.sort_by { Data === it.destination ? 2 : (it.plans&.any?(&:hash_keys) ? 1 : 0) }.each do |node|
|
|
320
|
+
@node = node
|
|
321
|
+
@current_object = node.original
|
|
322
|
+
node.plans&.each do |plan|
|
|
323
|
+
refresh(node, plan)
|
|
324
|
+
apply(node, plan) if plan.dirty
|
|
325
|
+
end
|
|
326
|
+
end
|
|
327
|
+
|
|
328
|
+
previous = component.map(&:result)
|
|
329
|
+
component.each do |node|
|
|
330
|
+
next if node.skipped
|
|
331
|
+
@node = node
|
|
332
|
+
@current_object = node.original
|
|
333
|
+
@replaying = true
|
|
334
|
+
node.result = catch(SKIP) { (node.callback || @callback).call(node.original, self) }
|
|
335
|
+
node.changed ||= !node.result.equal?(node.original)
|
|
336
|
+
ensure
|
|
337
|
+
@replaying = false
|
|
338
|
+
end
|
|
339
|
+
|
|
340
|
+
next unless component.each_with_index.all? { |node, index| node.result.equal?(previous[index]) }
|
|
341
|
+
component.each do |node|
|
|
342
|
+
next unless node.plans&.any?(&:dirty)
|
|
343
|
+
target = node.destination
|
|
344
|
+
target.rehash if Hash === target && !target.compare_by_identity?
|
|
345
|
+
target.reset if ::Set === target && !target.compare_by_identity?
|
|
346
|
+
end
|
|
347
|
+
return
|
|
348
|
+
end
|
|
349
|
+
|
|
350
|
+
raise ArgumentError, "cyclic modification did not converge"
|
|
351
|
+
end
|
|
352
|
+
end
|
|
353
|
+
private_constant :Modification
|
|
354
|
+
end
|
|
355
|
+
end
|
data/lib/farce/walker.rb
ADDED
|
@@ -0,0 +1,319 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
# shareable_constant_value: literal
|
|
3
|
+
# warn_indent: true
|
|
4
|
+
|
|
5
|
+
module Farce
|
|
6
|
+
# Visit an object graph or transform it with copies only where needed.
|
|
7
|
+
#
|
|
8
|
+
# Traversal follows container elements, hash keys and values, and ordinary
|
|
9
|
+
# instance variables. Farce containers expose their contents, not their storage.
|
|
10
|
+
# Proc traversal follows the receiver, not captured local variables.
|
|
11
|
+
# Module and class traversal includes directly defined public constants and
|
|
12
|
+
# class variables. Set constants: :inherited or class_variables: :inherited
|
|
13
|
+
# to include ancestors, or false to skip either kind. Autoloads are not loaded.
|
|
14
|
+
#
|
|
15
|
+
# {.visit} and {.modify} let the callback choose when to descend with {#traverse}.
|
|
16
|
+
# {.each} descends automatically and yields children before their parent.
|
|
17
|
+
# Shared children and cycles are tracked by identity.
|
|
18
|
+
#
|
|
19
|
+
# @example Scan a namespace without changing it
|
|
20
|
+
# namespace = Module.new
|
|
21
|
+
# namespace.const_set(:VALUE, [42])
|
|
22
|
+
# Farce::Walker.any?(namespace) { Integer === it } # => true
|
|
23
|
+
# Farce::Walker.any?(namespace, constants: false) { Integer === it } # => false
|
|
24
|
+
#
|
|
25
|
+
# @example Replace integers in a copy
|
|
26
|
+
# Farce::Walker.modify([1, [2]], copy: true) do |object, walker|
|
|
27
|
+
# Integer === object ? object + 1 : walker.traverse
|
|
28
|
+
# end # => [2, [3]]
|
|
29
|
+
class Walker
|
|
30
|
+
include Internal::MarshalSupport::Reject
|
|
31
|
+
|
|
32
|
+
module SendTo
|
|
33
|
+
private
|
|
34
|
+
|
|
35
|
+
def send_to(object, method, ...)
|
|
36
|
+
object.__send__(method, ...)
|
|
37
|
+
rescue NoMethodError => e
|
|
38
|
+
raise e unless e.name == method && e.receiver.equal?(object)
|
|
39
|
+
Kernel.instance_method(method).bind_call(object, ...)
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
class Visitor
|
|
44
|
+
include Shareable::Immutable
|
|
45
|
+
include SendTo
|
|
46
|
+
|
|
47
|
+
attr_reader :base_class
|
|
48
|
+
|
|
49
|
+
def initialize(base_class)
|
|
50
|
+
@base_class = base_class
|
|
51
|
+
super()
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def call(object, walker) = traverse(object, walker)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
include SendTo
|
|
58
|
+
|
|
59
|
+
REGISTER = ClassMirror.new(Visitor) { _1.new(_2) }
|
|
60
|
+
SKIP = Object.new.freeze
|
|
61
|
+
RETURN = Object.new.freeze
|
|
62
|
+
|
|
63
|
+
private_constant :SendTo, :Visitor, :REGISTER, :SKIP, :RETURN
|
|
64
|
+
private_class_method :new
|
|
65
|
+
|
|
66
|
+
# Register traversal for classes from the main Ractor.
|
|
67
|
+
#
|
|
68
|
+
# Subclasses inherit the nearest definition. Definitions can call `super`.
|
|
69
|
+
# Use {#update} to visit children and record assignments. Its block receives
|
|
70
|
+
# a writable target and transformed children. Return that target from the
|
|
71
|
+
# assignment block and return the update result from the definition.
|
|
72
|
+
#
|
|
73
|
+
# Should only be necessary for classes using storage defined outside of Ruby (like a C struct).
|
|
74
|
+
#
|
|
75
|
+
# @example
|
|
76
|
+
# # Lets assume NativeClass has a natively stored value
|
|
77
|
+
# Farce::Walker.define(NativeClass) do |object, walker|
|
|
78
|
+
# walker.update(object, [object.value]) do |target, values|
|
|
79
|
+
# target.value = values.first
|
|
80
|
+
# target
|
|
81
|
+
# end
|
|
82
|
+
# end
|
|
83
|
+
#
|
|
84
|
+
# @param classes [Array<Class>] classes to handle
|
|
85
|
+
# @yieldparam object [BasicObject] the object to traverse
|
|
86
|
+
# @yieldparam walker [Walker] the current walk
|
|
87
|
+
# @yieldreturn [BasicObject] the resulting object
|
|
88
|
+
# @note The block must be convertible to a shareable proc.
|
|
89
|
+
def self.define(*classes, &)
|
|
90
|
+
definition = Internal.prepare_method_definition(&)
|
|
91
|
+
raise LocalJumpError, "no block given" unless definition
|
|
92
|
+
classes.each { REGISTER.define(it) { define_method(:traverse, definition) } }
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# Visit an object without automatically descending or assigning replacements.
|
|
96
|
+
# @param object [BasicObject] the root object
|
|
97
|
+
# @!macro walker_module_options
|
|
98
|
+
# @param constants [Boolean, Symbol] true for directly defined public constants,
|
|
99
|
+
# :inherited to include ancestors, or false to skip. Autoloads are not loaded.
|
|
100
|
+
# @param class_variables [Boolean, Symbol] true for directly defined class variables,
|
|
101
|
+
# :inherited to include ancestors, or false to skip
|
|
102
|
+
# @yieldparam object [BasicObject] the current object
|
|
103
|
+
# @yieldparam walker [Walker] call {#traverse}, {#skip}, or {#return} to control the walk
|
|
104
|
+
# @yieldreturn [BasicObject] the result for the current object
|
|
105
|
+
# @return [BasicObject] the root callback's result, or the value passed to {#return}
|
|
106
|
+
# @raise [LocalJumpError] if no block is given
|
|
107
|
+
def self.visit(object, constants: true, class_variables: true, &callback)
|
|
108
|
+
raise LocalJumpError, "no block given" unless callback
|
|
109
|
+
catch(RETURN) { new(callback, false, false, constants, class_variables).visit(object) }
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
# Yield each reachable object once, after its children.
|
|
113
|
+
# @param object [BasicObject] the root object
|
|
114
|
+
# @!macro walker_module_options
|
|
115
|
+
# @yieldparam object [BasicObject] a reachable object
|
|
116
|
+
# @return [BasicObject, Enumerator] the root object, or an enumerator without a block
|
|
117
|
+
def self.each(object, constants: true, class_variables: true)
|
|
118
|
+
return enum_for(:each, object, constants:, class_variables:) unless block_given?
|
|
119
|
+
visit(object, constants:, class_variables:) do |object, walker|
|
|
120
|
+
walker.traverse(object)
|
|
121
|
+
yield(object)
|
|
122
|
+
object
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
# Stop at the first object for which the block is truthy.
|
|
127
|
+
# Without a block, test the objects themselves.
|
|
128
|
+
# @!macro walker_module_options
|
|
129
|
+
# @return [Boolean]
|
|
130
|
+
def self.any?(object, constants: true, class_variables: true, &)
|
|
131
|
+
each(object, constants:, class_variables:).any?(&)
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
# Stop at the first object for which the block is falsey.
|
|
135
|
+
# Without a block, test the objects themselves.
|
|
136
|
+
# @!macro walker_module_options
|
|
137
|
+
# @return [Boolean]
|
|
138
|
+
def self.all?(object, constants: true, class_variables: true, &)
|
|
139
|
+
each(object, constants:, class_variables:).all?(&)
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
# Transform a graph using the callback's return values.
|
|
143
|
+
# Call {#traverse} to transform an object's children. Returning another value
|
|
144
|
+
# replaces the object without automatically visiting that replacement.
|
|
145
|
+
# Unchanged branches retain their identity, including when copy is enabled.
|
|
146
|
+
# Changed cycles are connected before results are frozen or finalized.
|
|
147
|
+
# A cyclic callback may run again as child replacements become known. Keep
|
|
148
|
+
# callbacks repeatable and put publication or caching in {#finalize}.
|
|
149
|
+
# Use {#freeze_result} instead of freezing a preliminary traversal result.
|
|
150
|
+
# Data values are rebuilt only when members change, using their normal initializer.
|
|
151
|
+
# Noncopyable coordination objects, such as leases, are updated in place.
|
|
152
|
+
# Constants and class variables are assigned only when their value
|
|
153
|
+
# changes identity. Constant replacement can emit Ruby redefinition warnings.
|
|
154
|
+
# Replacing an inherited constant defines a local constant. Replacing an
|
|
155
|
+
# inherited class variable updates storage shared with its owner and siblings.
|
|
156
|
+
#
|
|
157
|
+
# A walk is not an atomic snapshot or update. Coordinate concurrent writers
|
|
158
|
+
# when transforming container structure, especially map keys.
|
|
159
|
+
# @param object [BasicObject] the root object
|
|
160
|
+
# @!macro walker_module_options
|
|
161
|
+
# @param copy [Boolean, Symbol] false to edit mutable objects in place, true to
|
|
162
|
+
# duplicate changed objects, or a copy method such as :clone. Unchanged objects
|
|
163
|
+
# may be shared with the input. Frozen objects are cloned only when changed.
|
|
164
|
+
# @param freeze [Boolean, nil] true to freeze results, false to avoid freezing them,
|
|
165
|
+
# or nil to preserve each original object's frozen state
|
|
166
|
+
# @yieldparam object [BasicObject] the original object
|
|
167
|
+
# @yieldparam walker [Walker] the current walk
|
|
168
|
+
# @yieldreturn [BasicObject] the replacement object
|
|
169
|
+
# @return [BasicObject] the transformed root, or the value passed to {#return}
|
|
170
|
+
# @raise [LocalJumpError] if no block is given
|
|
171
|
+
# @raise [ArgumentError] if copy is neither a boolean nor a Symbol
|
|
172
|
+
# @raise [ArgumentError] if cyclic callbacks do not converge within 32 passes,
|
|
173
|
+
# or a cyclic finalizer replaces its result
|
|
174
|
+
# @raise [ArgumentError] if a hash key or set element refers to a Data value
|
|
175
|
+
# still being constructed. Identity-based containers do not hash their keys.
|
|
176
|
+
def self.modify(object, copy: false, freeze: nil, constants: true, class_variables: true, &callback)
|
|
177
|
+
raise LocalJumpError, "no block given" unless callback
|
|
178
|
+
|
|
179
|
+
modify =
|
|
180
|
+
case copy
|
|
181
|
+
when false then true
|
|
182
|
+
when true then :dup
|
|
183
|
+
when Symbol then copy
|
|
184
|
+
else raise ArgumentError, "invalid value for copy: #{copy.inspect}"
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
catch(RETURN) { Modification.new(callback, modify, freeze, constants, class_variables).visit(object) }
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
# @return [false, true, Symbol] false for a read-only walk, true for in-place edits,
|
|
191
|
+
# or the selected copy method
|
|
192
|
+
attr_reader :modify
|
|
193
|
+
|
|
194
|
+
# @return [Boolean, Symbol] the constant traversal policy
|
|
195
|
+
attr_reader :constants
|
|
196
|
+
|
|
197
|
+
# @return [Boolean, Symbol] the class variable traversal policy
|
|
198
|
+
attr_reader :class_variables
|
|
199
|
+
|
|
200
|
+
# @return [BasicObject] the original object currently passed to the callback
|
|
201
|
+
attr_reader :current_object
|
|
202
|
+
|
|
203
|
+
# @!visibility private
|
|
204
|
+
def initialize(callback, modify, freeze, constants, class_variables)
|
|
205
|
+
{ constants:, class_variables: }.each do |name, policy|
|
|
206
|
+
next if [true, false, :inherited].include?(policy)
|
|
207
|
+
raise ArgumentError, "invalid value for #{name}: #{policy.inspect}"
|
|
208
|
+
end
|
|
209
|
+
@callback = callback
|
|
210
|
+
@modify = modify
|
|
211
|
+
@freeze = freeze
|
|
212
|
+
@constants = constants
|
|
213
|
+
@class_variables = class_variables
|
|
214
|
+
@visitors = {}.compare_by_identity
|
|
215
|
+
@seen = {}.compare_by_identity
|
|
216
|
+
@seen[UNDEFINED] = UNDEFINED
|
|
217
|
+
@current_object = UNDEFINED
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
# Visit a child, reusing the result if its identity has already been seen.
|
|
221
|
+
# @param object [BasicObject] the child object
|
|
222
|
+
# @return [BasicObject] the callback result or an in-progress copy for a cycle
|
|
223
|
+
def visit(object)
|
|
224
|
+
@seen.fetch(object) do
|
|
225
|
+
current_object = @current_object
|
|
226
|
+
@current_object = object
|
|
227
|
+
@seen[object] = object # set this first to stop recursion
|
|
228
|
+
@seen[object] = catch(SKIP) do
|
|
229
|
+
result = @callback.call(object, self)
|
|
230
|
+
case @modify && @freeze
|
|
231
|
+
when false then result
|
|
232
|
+
when true then send_to(result, :freeze)
|
|
233
|
+
else
|
|
234
|
+
send_to(result, :freeze) if send_to(object, :frozen?) && !send_to(result, :frozen?)
|
|
235
|
+
result
|
|
236
|
+
end
|
|
237
|
+
end
|
|
238
|
+
ensure
|
|
239
|
+
@current_object = current_object
|
|
240
|
+
end
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
# End the current callback and use object as its result without freezing it.
|
|
244
|
+
# @param object [BasicObject] the replacement, defaulting to the current object
|
|
245
|
+
# @return [void]
|
|
246
|
+
def skip(object = current_object) = throw(SKIP, object)
|
|
247
|
+
|
|
248
|
+
# End the whole walk immediately.
|
|
249
|
+
# @param object [BasicObject] the walk's result, defaulting to the current object
|
|
250
|
+
# @return [void]
|
|
251
|
+
def return(object = current_object) = throw(RETURN, object)
|
|
252
|
+
|
|
253
|
+
# Traverse an object's children using its registered class definition.
|
|
254
|
+
# @param object [BasicObject] the object to descend into, defaulting to the current object
|
|
255
|
+
# @return [BasicObject] the object with transformed children when modifying
|
|
256
|
+
def traverse(object = current_object)
|
|
257
|
+
klass = send_to(object, :class)
|
|
258
|
+
visitor = @visitors[klass] ||= REGISTER[klass]
|
|
259
|
+
visitor.call(object, self)
|
|
260
|
+
end
|
|
261
|
+
|
|
262
|
+
# @api private
|
|
263
|
+
def remember(object, result) = @seen[object] = result
|
|
264
|
+
|
|
265
|
+
# Visit child values and describe how to assign their replacements.
|
|
266
|
+
# The assignment block only runs during modification and may run again for cycles.
|
|
267
|
+
# It receives a writable target and the resolved children. Do not mutate object
|
|
268
|
+
# outside this block. Return the result of update from a traversal definition.
|
|
269
|
+
# @param object [BasicObject] the object being traversed
|
|
270
|
+
# @param values [Array] child references in assignment order
|
|
271
|
+
# @param hash_keys [Boolean] rebuild when key descendants change in place
|
|
272
|
+
# @param key_stride [Integer] spacing between keys in values, starting at zero
|
|
273
|
+
# @yieldparam target [BasicObject] the writable destination
|
|
274
|
+
# @yieldparam results [Array] transformed child references
|
|
275
|
+
# @return [BasicObject] the traversal result
|
|
276
|
+
def update(object, values, hash_keys: false, key_stride: 1) # rubocop:disable Lint/UnusedMethodArgument
|
|
277
|
+
values.each { visit(it) }
|
|
278
|
+
object
|
|
279
|
+
end
|
|
280
|
+
|
|
281
|
+
# Request freezing after the current result is settled.
|
|
282
|
+
# With copying enabled, mutable inputs are copied before freezing.
|
|
283
|
+
# @return [BasicObject] the current destination
|
|
284
|
+
# @raise [ArgumentError] outside a modifying walk
|
|
285
|
+
def freeze_result = raise(ArgumentError, "freeze_result requires a modifying walk")
|
|
286
|
+
|
|
287
|
+
# Register a finalizer for the current node. It runs once after resolution
|
|
288
|
+
# and requested freezing. Registering another finalizer replaces the first.
|
|
289
|
+
# Cyclic finalizers must preserve identity. Acyclic finalizers may return a
|
|
290
|
+
# canonical replacement, which is propagated to the parent.
|
|
291
|
+
# @yieldparam result [BasicObject] the settled result
|
|
292
|
+
# @yieldparam cyclic [Boolean] whether the node belongs to a cycle
|
|
293
|
+
# @yieldreturn [BasicObject] the final result
|
|
294
|
+
# @return [BasicObject] the current destination
|
|
295
|
+
# @raise [ArgumentError] outside a modifying walk
|
|
296
|
+
# @raise [LocalJumpError] if no block is given
|
|
297
|
+
def finalize(&) = raise(ArgumentError, "finalize requires a modifying walk")
|
|
298
|
+
|
|
299
|
+
# @api private
|
|
300
|
+
def rebuild_data(object)
|
|
301
|
+
object.class.members.each { visit(object.__send__(it)) }
|
|
302
|
+
object
|
|
303
|
+
end
|
|
304
|
+
|
|
305
|
+
# Whether a visited object or its traversed descendants changed.
|
|
306
|
+
# @param object [BasicObject] the object to inspect
|
|
307
|
+
# @return [Boolean] false for a read-only walk
|
|
308
|
+
def changed?(object = current_object) = false # rubocop:disable Lint/UnusedMethodArgument
|
|
309
|
+
|
|
310
|
+
# @api private
|
|
311
|
+
def hashable?(_object) = true
|
|
312
|
+
|
|
313
|
+
# @api private
|
|
314
|
+
def check_hash_key(object) = object
|
|
315
|
+
|
|
316
|
+
require "farce/walker/definitions"
|
|
317
|
+
require "farce/walker/modification"
|
|
318
|
+
end
|
|
319
|
+
end
|