lazy_init 0.1.1 → 0.2.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.
@@ -1,17 +1,13 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module LazyInit
4
- # Provides class-level methods for defining lazy attributes with various optimization strategies.
4
+ # Class-level methods for defining lazy attributes with Ruby version-specific optimizations.
5
5
  #
6
- # This module is automatically extended when a class includes or extends LazyInit.
7
- # It analyzes attribute configuration and selects the most efficient implementation:
8
- # simple inline methods for basic cases, optimized dependency methods for single
9
- # dependencies, and full LazyValue wrappers for complex scenarios.
10
- #
11
- # The module generates three methods for each lazy attribute:
12
- # - `attribute_name` - the main accessor method
13
- # - `attribute_name_computed?` - predicate to check computation state
14
- # - `reset_attribute_name!` - method to reset and allow recomputation
6
+ # Automatically selects the most efficient implementation:
7
+ # - Ruby 3+: eval-based methods for maximum performance
8
+ # - Ruby 2.6+: define_method with full compatibility
9
+ # - Simple cases: inline variables, dependency cases: lightweight resolution
10
+ # - Complex cases: full LazyValue with timeout and dependency support
15
11
  #
16
12
  # @example Basic lazy attribute
17
13
  # class ApiClient
@@ -22,52 +18,30 @@ module LazyInit
22
18
  # end
23
19
  # end
24
20
  #
25
- # @example Lazy attribute with dependencies
26
- # class DatabaseService
27
- # extend LazyInit
28
- #
29
- # lazy_attr_reader :config do
30
- # load_configuration
31
- # end
32
- #
33
- # lazy_attr_reader :connection, depends_on: [:config] do
34
- # Database.connect(config.database_url)
35
- # end
21
+ # @example With dependencies
22
+ # lazy_attr_reader :database, depends_on: [:config] do
23
+ # Database.connect(config.database_url)
36
24
  # end
37
25
  #
38
26
  # @since 0.1.0
39
27
  module ClassMethods
40
28
  # Set up necessary infrastructure when LazyInit is extended by a class.
41
29
  #
42
- # This is an internal Ruby hook method that's automatically called when a class
43
- # extends LazyInit. Users should never call this method directly - it's part of
44
- # Ruby's module extension mechanism.
45
- #
46
- # Initializes thread-safe mutex and dependency resolver for the target class.
47
- # This ensures each class has its own isolated dependency management.
48
- #
49
30
  # @param base [Class] the class being extended with LazyInit
50
31
  # @return [void]
51
32
  # @api private
52
33
  def self.extended(base)
53
34
  base.instance_variable_set(:@lazy_init_class_mutex, Mutex.new)
54
- base.instance_variable_set(:@dependency_resolver, DependencyResolver.new(base))
55
35
  end
56
36
 
57
- # Access the registry of all lazy initializers defined on this class.
58
- #
59
- # Used internally for introspection and debugging. Each entry contains
60
- # the configuration (block, timeout, dependencies) for a lazy attribute.
37
+ # Registry of all lazy initializers defined on this class.
61
38
  #
62
39
  # @return [Hash<Symbol, Hash>] mapping of attribute names to their configuration
63
40
  def lazy_initializers
64
41
  @lazy_initializers ||= {}
65
42
  end
66
43
 
67
- # Access the dependency resolver for this class.
68
- #
69
- # Handles dependency graph management and resolution order computation.
70
- # Creates a new resolver if one doesn't exist.
44
+ # Lazy dependency resolver - created only when needed for performance.
71
45
  #
72
46
  # @return [DependencyResolver] the resolver instance for this class
73
47
  def dependency_resolver
@@ -76,10 +50,11 @@ module LazyInit
76
50
 
77
51
  # Define a thread-safe lazy-initialized instance attribute.
78
52
  #
79
- # The attribute will be computed only once per instance when first accessed.
80
- # Subsequent calls return the cached value. The implementation is automatically
81
- # optimized based on complexity: simple cases use inline variables, single
82
- # dependencies use optimized resolution, complex cases use full LazyValue.
53
+ # Automatically optimizes based on Ruby version and complexity:
54
+ # - Ruby 3+: uses eval for maximum performance
55
+ # - Simple cases: direct instance variables
56
+ # - Dependencies: lightweight resolution for single deps, full resolver for complex
57
+ # - Timeouts: full LazyValue wrapper
83
58
  #
84
59
  # @param name [Symbol, String] the attribute name
85
60
  # @param timeout [Numeric, nil] timeout in seconds for the computation
@@ -107,7 +82,7 @@ module LazyInit
107
82
  validate_attribute_name!(name)
108
83
  raise ArgumentError, 'Block is required' unless block
109
84
 
110
- # store configuration for introspection and debugging
85
+ # store configuration for introspection
111
86
  config = {
112
87
  block: block,
113
88
  timeout: timeout || LazyInit.configuration.default_timeout,
@@ -118,27 +93,36 @@ module LazyInit
118
93
  # register dependencies with resolver if present
119
94
  dependency_resolver.add_dependency(name, depends_on) if depends_on
120
95
 
121
- # select optimal implementation strategy based on complexity
122
- if enhanced_simple_case?(timeout, depends_on)
96
+ # select optimal implementation strategy
97
+ if depends_on && Array(depends_on).size == 1 && !timeout
98
+ generate_simple_dependency_with_inline_check(name, Array(depends_on).first, block)
99
+ generate_predicate_method(name)
100
+ generate_reset_method(name)
101
+ elsif depends_on && Array(depends_on).size > 1 && !timeout
102
+ generate_fast_dependency_method(name, depends_on, block, config)
103
+ generate_predicate_method(name)
104
+ generate_reset_method_with_deps_flag(name)
105
+ elsif simple_case_eligible?(timeout, depends_on)
106
+ generate_optimized_simple_method(name, block)
107
+ elsif enhanced_simple_case?(timeout, depends_on)
123
108
  if simple_dependency_case?(depends_on)
124
- generate_simple_dependency_method(name, depends_on, block)
109
+ generate_lazy_compiling_method(name, block, :dependency, depends_on)
125
110
  else
126
- generate_simple_inline_method(name, block)
111
+ generate_lazy_compiling_method(name, block, :simple, nil)
127
112
  end
113
+ generate_predicate_method(name)
114
+ generate_reset_method(name)
128
115
  else
129
116
  generate_complex_lazyvalue_method(name, config)
117
+ generate_predicate_method(name)
118
+ generate_reset_method(name)
130
119
  end
131
-
132
- # generate helper methods for all implementation types
133
- generate_predicate_method(name)
134
- generate_reset_method(name)
135
120
  end
136
121
 
137
122
  # Define a thread-safe lazy-initialized class variable shared across all instances.
138
123
  #
139
- # The variable will be computed only once per class when first accessed.
140
- # All instances share the same computed value. Class variables are always
141
- # implemented using LazyValue for full thread safety and feature support.
124
+ # Uses full LazyValue wrapper for thread safety and feature completeness.
125
+ # All instances share the same computed value.
142
126
  #
143
127
  # @param name [Symbol, String] the class variable name
144
128
  # @param timeout [Numeric, nil] timeout in seconds for the computation
@@ -158,7 +142,7 @@ module LazyInit
158
142
 
159
143
  class_variable_name = "@@#{name}_lazy_value"
160
144
 
161
- # register dependencies for class-level attributes too
145
+ # register dependencies for class-level attributes
162
146
  dependency_resolver.add_dependency(name, depends_on) if depends_on
163
147
 
164
148
  # cache configuration for use in generated methods
@@ -215,20 +199,43 @@ module LazyInit
215
199
 
216
200
  private
217
201
 
218
- # Determine if an attribute qualifies for simple optimization.
202
+ # Generate optimized methods based on dependency type and Ruby version.
219
203
  #
220
- # Simple cases avoid LazyValue overhead by using direct instance variables.
221
- # This includes attributes with no timeout and either no dependencies or
222
- # a single simple dependency that can be inlined.
204
+ # @param name [Symbol] the attribute name
205
+ # @param block [Proc] the computation block
206
+ # @param dependency_type [Symbol] :simple or :dependency
207
+ # @param depends_on [Array<Symbol>, Symbol, nil] dependencies for :dependency type
208
+ # @return [void]
209
+ # @api private
210
+ def generate_lazy_compiling_method(name, block, dependency_type = :simple, depends_on = nil)
211
+ case dependency_type
212
+ when :dependency
213
+ if depends_on && Array(depends_on).size == 1
214
+ # single dependency: fast path with lightweight resolution
215
+ generate_simple_dependency_with_resolution(name, depends_on, block)
216
+ else
217
+ # complex dependency: full LazyValue with complex resolution
218
+ config = { block: block, timeout: nil, depends_on: depends_on }
219
+ generate_complex_lazyvalue_method(name, config)
220
+ end
221
+ when :simple
222
+ # no dependencies: fastest path
223
+ if LazyInit::RubyCapabilities::IMPROVED_EVAL_PERFORMANCE
224
+ generate_simple_inline_method_with_eval(name, block)
225
+ else
226
+ generate_simple_inline_method_with_define_method(name, block)
227
+ end
228
+ end
229
+ end
230
+
231
+ # Check if attribute qualifies for simple optimization (no timeout, simple dependencies).
223
232
  #
224
233
  # @param timeout [Object] timeout configuration
225
234
  # @param depends_on [Object] dependency configuration
226
235
  # @return [Boolean] true if simple implementation should be used
227
236
  def enhanced_simple_case?(timeout, depends_on)
228
- # timeout requires LazyValue for proper handling
229
237
  return false unless timeout.nil?
230
238
 
231
- # categorize dependency complexity
232
239
  case depends_on
233
240
  when nil, []
234
241
  true # no dependencies are always simple
@@ -243,150 +250,196 @@ module LazyInit
243
250
 
244
251
  # Check if dependencies qualify for simple dependency optimization.
245
252
  #
246
- # Single dependencies can use an optimized resolution strategy that
247
- # avoids the full dependency resolver overhead.
248
- #
249
253
  # @param depends_on [Object] dependency configuration
250
254
  # @return [Boolean] true if simple dependency method should be used
251
255
  def simple_dependency_case?(depends_on)
252
256
  return false if depends_on.nil? || depends_on.empty?
253
257
 
254
- deps = Array(depends_on)
255
- deps.size == 1 # any single dependency qualifies for optimization
258
+ Array(depends_on).size == 1
256
259
  end
257
260
 
258
- # Generate an optimized method for attributes with single dependencies.
259
- #
260
- # This creates a method that uses inline variables for storage and
261
- # optimized dependency resolution that avoids LazyValue overhead.
262
- # Includes circular dependency detection and thread-safe error caching.
261
+ # Generate full LazyValue method for complex scenarios (timeout, multiple dependencies).
263
262
  #
264
263
  # @param name [Symbol] the attribute name
265
- # @param depends_on [Array, Symbol] the single dependency
266
- # @param block [Proc] the computation block
264
+ # @param config [Hash] the attribute configuration
267
265
  # @return [void]
268
- def generate_simple_dependency_method(name, depends_on, block)
269
- dep_name = Array(depends_on).first
270
- computed_var = "@#{name}_computed"
271
- value_var = "@#{name}_value"
272
- exception_var = "@#{name}_exception"
273
-
274
- # cache references to avoid repeated lookups in generated method
275
- cached_block = block
276
- cached_dep_name = dep_name
266
+ def generate_complex_lazyvalue_method(name, config)
267
+ cached_timeout = config[:timeout]
268
+ cached_depends_on = config[:depends_on]
269
+ cached_block = config[:block]
277
270
 
278
271
  define_method(name) do
279
- # fast path: return cached result including cached errors
280
- if instance_variable_get(computed_var)
281
- stored_exception = instance_variable_get(exception_var)
282
- raise stored_exception if stored_exception
272
+ # resolve dependencies using full dependency resolver
273
+ self.class.dependency_resolver.resolve_dependencies(name, self) if cached_depends_on
283
274
 
284
- return instance_variable_get(value_var)
275
+ # lazy creation of LazyValue wrapper
276
+ ivar_name = "@#{name}_lazy_value"
277
+ lazy_value = instance_variable_get(ivar_name) if instance_variable_defined?(ivar_name)
278
+
279
+ unless lazy_value
280
+ lazy_value = LazyValue.new(timeout: cached_timeout) do
281
+ instance_eval(&cached_block)
282
+ end
283
+ instance_variable_set(ivar_name, lazy_value)
285
284
  end
286
285
 
287
- # circular dependency protection using shared resolution stack
288
- resolution_stack = Thread.current[:lazy_init_resolution_stack] ||= []
289
- if resolution_stack.include?(name)
290
- circular_error = LazyInit::DependencyError.new(
291
- "Circular dependency detected: #{resolution_stack.join(' -> ')} -> #{name}"
292
- )
286
+ lazy_value.value
287
+ end
288
+ end
293
289
 
294
- # thread-safe error caching so all threads see the same error
295
- mutex = self.class.instance_variable_get(:@lazy_init_simple_mutex) || Mutex.new
296
- unless self.class.instance_variable_get(:@lazy_init_simple_mutex)
297
- self.class.instance_variable_set(:@lazy_init_simple_mutex, mutex)
298
- end
290
+ # Generate predicate method to check computation state.
291
+ # Handles both simple (inline variables) and complex (LazyValue) cases.
292
+ #
293
+ # @param name [Symbol] the attribute name
294
+ # @return [void]
295
+ def generate_predicate_method(name)
296
+ define_method("#{name}_computed?") do
297
+ # check simple implementation first
298
+ computed_var = "@#{name}_computed"
299
+ exception_var = "@#{name}_exception"
299
300
 
300
- mutex.synchronize do
301
- instance_variable_set(exception_var, circular_error)
302
- instance_variable_set(computed_var, true)
303
- end
301
+ if instance_variable_defined?(computed_var)
302
+ # simple implementation: computed but not if there's a cached exception
303
+ return instance_variable_get(computed_var) && !instance_variable_get(exception_var)
304
+ end
304
305
 
305
- raise circular_error
306
+ # check complex implementation (LazyValue wrapper)
307
+ lazy_var = "@#{name}_lazy_value"
308
+ if instance_variable_defined?(lazy_var)
309
+ lazy_value = instance_variable_get(lazy_var)
310
+ return lazy_value&.computed? || false
306
311
  end
307
312
 
308
- # ensure we have a mutex for thread-safe computation
309
- mutex = self.class.instance_variable_get(:@lazy_init_simple_mutex)
310
- unless mutex
311
- mutex = Mutex.new
312
- self.class.instance_variable_set(:@lazy_init_simple_mutex, mutex)
313
+ false
314
+ end
315
+ end
316
+
317
+ # Generate reset method to clear computed state and allow recomputation.
318
+ # Handles both simple and complex implementations.
319
+ #
320
+ # @param name [Symbol] the attribute name
321
+ # @return [void]
322
+ def generate_reset_method(name)
323
+ define_method("reset_#{name}!") do
324
+ # handle simple implementation reset
325
+ computed_var = "@#{name}_computed"
326
+ value_var = "@#{name}_value"
327
+ exception_var = "@#{name}_exception"
328
+
329
+ if instance_variable_defined?(computed_var)
330
+ # use mutex if available for thread safety
331
+ mutex = self.class.instance_variable_get(:@lazy_init_simple_mutex)
332
+ if mutex
333
+ mutex.synchronize do
334
+ instance_variable_set(computed_var, false)
335
+ remove_instance_variable(value_var) if instance_variable_defined?(value_var)
336
+ remove_instance_variable(exception_var) if instance_variable_defined?(exception_var)
337
+ end
338
+ else
339
+ instance_variable_set(computed_var, false)
340
+ remove_instance_variable(value_var) if instance_variable_defined?(value_var)
341
+ remove_instance_variable(exception_var) if instance_variable_defined?(exception_var)
342
+ end
343
+ return
313
344
  end
314
345
 
315
- # track this attribute in resolution stack
316
- resolution_stack.push(name)
346
+ # handle complex implementation reset (LazyValue)
347
+ lazy_var = "@#{name}_lazy_value"
348
+ if instance_variable_defined?(lazy_var)
349
+ lazy_value = instance_variable_get(lazy_var)
350
+ lazy_value&.reset!
351
+ remove_instance_variable(lazy_var)
352
+ end
353
+ end
354
+ end
355
+
356
+ # Validate attribute name follows Ruby conventions.
357
+ #
358
+ # @param name [Object] the proposed attribute name
359
+ # @return [void]
360
+ # @raise [InvalidAttributeNameError] if the name is invalid
361
+ def validate_attribute_name!(name)
362
+ raise InvalidAttributeNameError, 'Attribute name cannot be nil' if name.nil?
363
+ raise InvalidAttributeNameError, 'Attribute name cannot be empty' if name.to_s.strip.empty?
364
+
365
+ unless name.is_a?(Symbol) || name.is_a?(String)
366
+ raise InvalidAttributeNameError, 'Attribute name must be a symbol or string'
367
+ end
368
+
369
+ name_str = name.to_s
370
+ return if name_str.match?(/\A[a-zA-Z_][a-zA-Z0-9_]*[?!]?\z/)
371
+
372
+ raise InvalidAttributeNameError, "Invalid attribute name: #{name_str}"
373
+ end
374
+
375
+ # Ruby 3+ eval-based method generation for simple methods (no dependencies).
376
+ # Optimized for maximum performance with minimal overhead.
377
+ #
378
+ # @param name [Symbol] attribute name
379
+ # @param block [Proc] computation block
380
+ # @return [void]
381
+ def generate_simple_inline_method_with_eval(name, block)
382
+ block_var = "@@lazy_#{name}_block_#{object_id}"
383
+ class_variable_set(block_var, block)
384
+
385
+ method_code = <<~RUBY
386
+ def #{name}
387
+ # fast path: return cached value immediately if available
388
+ return @#{name}_value if @#{name}_computed
389
+
390
+ # shared mutex for thread safety - avoid per-method mutex overhead
391
+ mutex = self.class.instance_variable_get(:@lazy_init_simple_mutex)
392
+ unless mutex
393
+ mutex = Mutex.new
394
+ self.class.instance_variable_set(:@lazy_init_simple_mutex, mutex)
395
+ end
317
396
 
318
- begin
319
397
  mutex.synchronize do
320
- # double-check pattern after acquiring lock
321
- if instance_variable_get(computed_var)
322
- stored_exception = instance_variable_get(exception_var)
398
+ # double-check pattern: another thread might have computed while we waited
399
+ if @#{name}_computed
400
+ stored_exception = @#{name}_exception
323
401
  raise stored_exception if stored_exception
324
-
325
- return instance_variable_get(value_var)
402
+ return @#{name}_value
326
403
  end
327
404
 
328
405
  begin
329
- # ensure dependency is computed first using optimized approach
330
- unless send("#{cached_dep_name}_computed?")
331
- # temporarily release lock to avoid deadlocks during dependency resolution
332
- mutex.unlock
333
- begin
334
- send(cached_dep_name) # uses same shared resolution_stack for circular detection
335
- ensure
336
- mutex.lock
337
- end
338
-
339
- # check if we got computed while lock was released
340
- if instance_variable_get(computed_var)
341
- stored_exception = instance_variable_get(exception_var)
342
- raise stored_exception if stored_exception
343
-
344
- return instance_variable_get(value_var)
345
- end
346
- end
347
-
348
- # perform the actual computation with dependency available
349
- result = instance_eval(&cached_block)
350
- instance_variable_set(value_var, result)
351
- instance_variable_set(computed_var, true)
406
+ # perform computation and cache result
407
+ block = self.class.class_variable_get(:#{block_var})
408
+ result = instance_eval(&block)
409
+ @#{name}_value = result
410
+ @#{name}_computed = true
352
411
  result
353
412
  rescue StandardError => e
354
- # cache exceptions for consistent behavior across threads
355
- instance_variable_set(exception_var, e)
356
- instance_variable_set(computed_var, true)
413
+ # cache exceptions to ensure consistent error behavior
414
+ @#{name}_exception = e
415
+ @#{name}_computed = true
357
416
  raise
358
417
  end
359
418
  end
360
- ensure
361
- # always clean up resolution stack to prevent leaks
362
- resolution_stack.pop
363
- Thread.current[:lazy_init_resolution_stack] = nil if resolution_stack.empty?
364
419
  end
365
- end
420
+ RUBY
421
+
422
+ class_eval(method_code)
366
423
  end
367
424
 
368
- # Generate a simple inline method for attributes with no dependencies.
425
+ # Ruby 2.6+ fallback using define_method for simple methods.
426
+ # Compatible version of the eval-based simple method.
369
427
  #
370
- # Uses direct instance variables for maximum performance while maintaining
371
- # thread safety through mutex synchronization. This is the fastest
372
- # implementation strategy available.
373
- #
374
- # @param name [Symbol] the attribute name
375
- # @param block [Proc] the computation block
428
+ # @param name [Symbol] attribute name
429
+ # @param block [Proc] computation block
376
430
  # @return [void]
377
- def generate_simple_inline_method(name, block)
431
+ def generate_simple_inline_method_with_define_method(name, block)
378
432
  computed_var = "@#{name}_computed"
379
433
  value_var = "@#{name}_value"
380
434
  exception_var = "@#{name}_exception"
381
435
 
382
- # cache block reference to avoid lookup in generated method
383
436
  cached_block = block
384
437
 
385
438
  define_method(name) do
386
439
  # fast path: return cached value immediately if available
387
440
  return instance_variable_get(value_var) if instance_variable_get(computed_var)
388
441
 
389
- # ensure we have a shared mutex for thread safety
442
+ # shared mutex for thread safety
390
443
  mutex = self.class.instance_variable_get(:@lazy_init_simple_mutex)
391
444
  unless mutex
392
445
  mutex = Mutex.new
@@ -418,132 +471,402 @@ module LazyInit
418
471
  end
419
472
  end
420
473
 
421
- # Generate a method using full LazyValue for complex scenarios.
474
+ # Generate single dependency method with lightweight resolution.
475
+ # Uses eval or define_method based on Ruby version capabilities.
422
476
  #
423
- # This handles timeouts, complex dependencies, and other advanced features
424
- # that require the full LazyValue implementation. Used when simple
425
- # optimizations aren't applicable.
477
+ # @param name [Symbol] attribute name
478
+ # @param depends_on [Array<Symbol>, Symbol] dependency specification
479
+ # @param block [Proc] computation block
480
+ # @return [void]
481
+ def generate_simple_dependency_with_resolution(name, depends_on, block)
482
+ dep_name = Array(depends_on).first
483
+ dependency_resolver.add_dependency(name, depends_on)
484
+
485
+ if LazyInit::RubyCapabilities::IMPROVED_EVAL_PERFORMANCE
486
+ generate_fast_dependency_method_with_eval(name, dep_name, block)
487
+ else
488
+ generate_fast_dependency_method_with_define_method(name, dep_name, block)
489
+ generate_predicate_method(name)
490
+ generate_reset_method(name)
491
+ end
492
+ end
493
+
494
+ # Ruby 3+ eval-based fast dependency method with circular detection.
495
+ # Includes predicate and reset methods in single eval call for performance.
426
496
  #
427
- # @param name [Symbol] the attribute name
428
- # @param config [Hash] the attribute configuration
497
+ # @param name [Symbol] attribute name
498
+ # @param dep_name [Symbol] dependency attribute name
499
+ # @param block [Proc] computation block
429
500
  # @return [void]
430
- def generate_complex_lazyvalue_method(name, config)
431
- # cache configuration to avoid hash lookups in generated method
432
- cached_timeout = config[:timeout]
433
- cached_depends_on = config[:depends_on]
434
- cached_block = config[:block]
501
+ def generate_fast_dependency_method_with_eval(name, dep_name, block)
502
+ block_var = "@@lazy_#{name}_block_#{object_id}"
503
+ class_variable_set(block_var, block)
504
+
505
+ method_code = <<~RUBY
506
+ def #{name}
507
+ if @#{name}_computed
508
+ stored_exception = @#{name}_exception
509
+ raise stored_exception if stored_exception
510
+ return @#{name}_value
511
+ end
512
+
513
+ # circular dependency detection
514
+ resolution_stack = Thread.current[:lazy_init_resolution_stack] ||= []
515
+ if resolution_stack.include?(:#{name})
516
+ circular_error = LazyInit::DependencyError.new(
517
+ "Circular dependency detected: \#{resolution_stack.join(' -> ')} -> #{name}"
518
+ )
519
+ @#{name}_exception = circular_error
520
+ @#{name}_computed = true
521
+ raise circular_error
522
+ end
523
+
524
+ # lightweight dependency resolution with circular protection
525
+ resolution_stack.push(:#{name})
526
+ begin
527
+ #{dep_name} unless #{dep_name}_computed?
528
+ ensure
529
+ resolution_stack.pop
530
+ Thread.current[:lazy_init_resolution_stack] = nil if resolution_stack.empty?
531
+ end
532
+
533
+ @#{name}_mutex ||= Mutex.new
534
+ @#{name}_mutex.synchronize do
535
+ if @#{name}_computed
536
+ stored_exception = @#{name}_exception#{' '}
537
+ raise stored_exception if stored_exception
538
+ return @#{name}_value
539
+ end
540
+
541
+ begin
542
+ block = self.class.class_variable_get(:#{block_var})
543
+ result = instance_eval(&block)
544
+ @#{name}_value = result
545
+ @#{name}_computed = true
546
+ result
547
+ rescue StandardError => e
548
+ @#{name}_exception = e
549
+ @#{name}_computed = true
550
+ raise
551
+ end
552
+ end
553
+ end
554
+
555
+ # generate compatible predicate method
556
+ def #{name}_computed?
557
+ @#{name}_computed && !@#{name}_exception
558
+ end
559
+
560
+ # generate compatible reset method
561
+ def reset_#{name}!
562
+ @#{name}_mutex&.synchronize do
563
+ @#{name}_computed = false
564
+ @#{name}_value = nil
565
+ @#{name}_exception = nil
566
+ end
567
+ end
568
+ RUBY
569
+
570
+ class_eval(method_code)
571
+ end
572
+
573
+ # Ruby 2.6+ define_method version of fast dependency method.
574
+ # Provides same functionality as eval version with full compatibility.
575
+ #
576
+ # @param name [Symbol] attribute name
577
+ # @param dep_name [Symbol] dependency attribute name
578
+ # @param block [Proc] computation block
579
+ # @return [void]
580
+ def generate_fast_dependency_method_with_define_method(name, dep_name, block)
581
+ computed_var = "@#{name}_computed"
582
+ value_var = "@#{name}_value"
583
+ exception_var = "@#{name}_exception"
584
+
585
+ cached_block = block
586
+ cached_dep_name = dep_name
435
587
 
436
588
  define_method(name) do
437
- # resolve dependencies using full dependency resolver if needed
438
- self.class.dependency_resolver.resolve_dependencies(name, self) if cached_depends_on
589
+ # fast path with exception check
590
+ if instance_variable_get(computed_var)
591
+ stored_exception = instance_variable_get(exception_var)
592
+ raise stored_exception if stored_exception
439
593
 
440
- # lazy creation of LazyValue wrapper
441
- ivar_name = "@#{name}_lazy_value"
442
- lazy_value = instance_variable_get(ivar_name) if instance_variable_defined?(ivar_name)
594
+ return instance_variable_get(value_var)
595
+ end
443
596
 
444
- unless lazy_value
445
- lazy_value = LazyValue.new(timeout: cached_timeout) do
446
- instance_eval(&cached_block)
597
+ # circular dependency detection
598
+ resolution_stack = Thread.current[:lazy_init_resolution_stack] ||= []
599
+ if resolution_stack.include?(name)
600
+ circular_error = LazyInit::DependencyError.new(
601
+ "Circular dependency detected: #{resolution_stack.join(' -> ')} -> #{name}"
602
+ )
603
+ # cache the error
604
+ instance_variable_set(exception_var, circular_error)
605
+ instance_variable_set(computed_var, true)
606
+ raise circular_error
607
+ end
608
+
609
+ # lightweight dependency resolution with circular protection
610
+ resolution_stack.push(name)
611
+ begin
612
+ send(cached_dep_name) unless send("#{cached_dep_name}_computed?")
613
+ ensure
614
+ resolution_stack.pop
615
+ Thread.current[:lazy_init_resolution_stack] = nil if resolution_stack.empty?
616
+ end
617
+
618
+ # thread-safe computation
619
+ mutex = self.class.instance_variable_get(:@lazy_init_simple_mutex)
620
+ unless mutex
621
+ mutex = Mutex.new
622
+ self.class.instance_variable_set(:@lazy_init_simple_mutex, mutex)
623
+ end
624
+
625
+ mutex.synchronize do
626
+ if instance_variable_get(computed_var)
627
+ stored_exception = instance_variable_get(exception_var)
628
+ raise stored_exception if stored_exception
629
+
630
+ return instance_variable_get(value_var)
631
+ end
632
+
633
+ begin
634
+ result = instance_eval(&cached_block)
635
+ instance_variable_set(value_var, result)
636
+ instance_variable_set(computed_var, true)
637
+ result
638
+ rescue StandardError => e
639
+ instance_variable_set(exception_var, e)
640
+ instance_variable_set(computed_var, true)
641
+ raise
447
642
  end
448
- instance_variable_set(ivar_name, lazy_value)
449
643
  end
644
+ end
645
+ end
450
646
 
451
- lazy_value.value
647
+ def simple_case_eligible?(timeout, depends_on)
648
+ timeout.nil? &&
649
+ (depends_on.nil? || depends_on.empty?) && LazyInit::RubyCapabilities::RUBY_3_PLUS
650
+ end
651
+
652
+ def generate_optimized_simple_method(name, block)
653
+ if LazyInit::RubyCapabilities::RUBY_3_PLUS
654
+ generate_ruby3_ultra_simple_method(name, block)
655
+ else
656
+ # Fallback to existing implementation
657
+ generate_simple_inline_method_with_define_method(name, block)
452
658
  end
659
+
660
+ generate_simple_helpers(name)
453
661
  end
454
662
 
455
- # Generate predicate method to check if attribute has been computed.
663
+ # Generate ultra-optimized method for Ruby 3+ simple cases.
456
664
  #
457
- # Handles both simple (inline variables) and complex (LazyValue) cases.
458
- # Returns false for exceptions to maintain consistent behavior.
665
+ # Uses eval-based method generation with shared mutex and direct
666
+ # instance variable access for maximum performance. Stores computation
667
+ # block in class variable for fast access.
459
668
  #
460
- # @param name [Symbol] the attribute name
669
+ # @param name [Symbol] attribute name
670
+ # @param block [Proc] computation block
461
671
  # @return [void]
462
- def generate_predicate_method(name)
463
- define_method("#{name}_computed?") do
464
- # check simple implementation first (most common after optimization)
465
- computed_var = "@#{name}_computed"
466
- exception_var = "@#{name}_exception"
672
+ # @api private
673
+ def generate_ruby3_ultra_simple_method(name, block)
674
+ ensure_shared_mutex
675
+
676
+ block_var = "@@simple_#{name}_#{object_id}"
677
+ class_variable_set(block_var, block)
467
678
 
468
- if instance_variable_defined?(computed_var)
469
- # simple implementation: computed but not if there's a cached exception
470
- return instance_variable_get(computed_var) && !instance_variable_get(exception_var)
471
- end
679
+ method_code = <<~RUBY
680
+ def #{name}
681
+ return @#{name}_value if defined?(@#{name}_value)
472
682
 
473
- # check complex implementation (LazyValue wrapper)
474
- lazy_var = "@#{name}_lazy_value"
475
- if instance_variable_defined?(lazy_var)
476
- lazy_value = instance_variable_get(lazy_var)
477
- return lazy_value&.computed? || false
683
+ shared_mutex = self.class.instance_variable_get(:@shared_mutex)
684
+ shared_mutex.synchronize do
685
+ return @#{name}_value if defined?(@#{name}_value)
686
+ raise @#{name}_exception if defined?(@#{name}_exception)
687
+
688
+ begin
689
+ @#{name}_value = instance_eval(&self.class.class_variable_get(:#{block_var}))
690
+ rescue StandardError => e
691
+ @#{name}_exception = e
692
+ raise
693
+ end
694
+ end
478
695
  end
696
+ RUBY
479
697
 
480
- # not computed yet
481
- false
482
- end
698
+ class_eval(method_code)
483
699
  end
484
700
 
485
- # Generate reset method to clear computed state and allow recomputation.
701
+ # Generate predicate and reset helper methods for simple cases.
486
702
  #
487
- # Handles both simple and complex implementations, ensuring proper
488
- # cleanup of all associated state including cached exceptions.
703
+ # Creates computed? and reset! methods that work with the direct
704
+ # instance variable approach used by simple case optimization.
489
705
  #
490
- # @param name [Symbol] the attribute name
706
+ # @param name [Symbol] attribute name
491
707
  # @return [void]
492
- def generate_reset_method(name)
708
+ # @api private
709
+ def generate_simple_helpers(name)
710
+ define_method("#{name}_computed?") do
711
+ instance_variable_defined?("@#{name}_value") && !instance_variable_defined?("@#{name}_exception")
712
+ end
713
+
493
714
  define_method("reset_#{name}!") do
494
- # handle simple implementation reset
495
- computed_var = "@#{name}_computed"
496
- value_var = "@#{name}_value"
497
- exception_var = "@#{name}_exception"
715
+ shared_mutex = self.class.instance_variable_get(:@shared_mutex)
716
+ shared_mutex.synchronize do
717
+ remove_instance_variable("@#{name}_value") if instance_variable_defined?("@#{name}_value")
718
+ remove_instance_variable("@#{name}_exception") if instance_variable_defined?("@#{name}_exception")
719
+ end
720
+ end
721
+ end
498
722
 
499
- if instance_variable_defined?(computed_var)
500
- # use mutex if available for thread safety during reset
501
- mutex = self.class.instance_variable_get(:@lazy_init_simple_mutex)
502
- if mutex
503
- mutex.synchronize do
504
- instance_variable_set(computed_var, false)
505
- remove_instance_variable(value_var) if instance_variable_defined?(value_var)
506
- remove_instance_variable(exception_var) if instance_variable_defined?(exception_var)
723
+ # Ensure shared mutex exists for simple case optimization.
724
+ #
725
+ # Creates a class-level mutex shared by all simple attributes to
726
+ # reduce memory overhead compared to per-attribute mutexes.
727
+ #
728
+ # @return [void]
729
+ # @api private
730
+ def ensure_shared_mutex
731
+ return if instance_variable_defined?(:@shared_mutex)
732
+ @shared_mutex = Mutex.new
733
+ end
734
+
735
+ # Generate optimized method for single dependency attributes.
736
+ #
737
+ # Bypasses dependency resolver overhead by directly checking and
738
+ # resolving single dependencies inline. Includes circular dependency
739
+ # detection and thread-safe computation.
740
+ #
741
+ # @param name [Symbol] attribute name
742
+ # @param dep_name [Symbol] dependency attribute name
743
+ # @param block [Proc] computation block
744
+ # @return [void]
745
+ # @api private
746
+ def generate_simple_dependency_with_inline_check(name, dep_name, block)
747
+ cached_block = block
748
+ cached_dep_name = dep_name
749
+ computed_var = "@#{name}_computed"
750
+ value_var = "@#{name}_value"
751
+ exception_var = "@#{name}_exception"
752
+
753
+ define_method(name) do
754
+ # Fast path: return cached result
755
+ return instance_variable_get(value_var) if instance_variable_get(computed_var)
756
+
757
+ # Circular dependency detection BEFORE mutex
758
+ resolution_stack = Thread.current[:lazy_init_resolution_stack] ||= []
759
+ if resolution_stack.include?(name)
760
+ circular_error = LazyInit::DependencyError.new(
761
+ "Circular dependency detected: #{resolution_stack.join(' -> ')} -> #{name}"
762
+ )
763
+ raise circular_error
764
+ end
765
+
766
+ resolution_stack.push(name)
767
+ begin
768
+ # Inline dependency check
769
+ unless send("#{cached_dep_name}_computed?")
770
+ send(cached_dep_name)
771
+ end
772
+
773
+ # Thread-safe computation
774
+ mutex = self.class.instance_variable_get(:@lazy_init_class_mutex)
775
+ mutex.synchronize do
776
+ return instance_variable_get(value_var) if instance_variable_get(computed_var)
777
+
778
+ begin
779
+ result = instance_eval(&cached_block)
780
+ instance_variable_set(value_var, result)
781
+ instance_variable_set(computed_var, true)
782
+ result
783
+ rescue StandardError => e
784
+ instance_variable_set(exception_var, e)
785
+ instance_variable_set(computed_var, true)
786
+ raise
507
787
  end
508
- else
509
- # no mutex means no concurrent access, safe to reset directly
510
- instance_variable_set(computed_var, false)
511
- remove_instance_variable(value_var) if instance_variable_defined?(value_var)
512
- remove_instance_variable(exception_var) if instance_variable_defined?(exception_var)
513
788
  end
514
- return
789
+ ensure
790
+ resolution_stack.pop
791
+ Thread.current[:lazy_init_resolution_stack] = nil if resolution_stack.empty?
515
792
  end
793
+ end
794
+ end
516
795
 
517
- # handle complex implementation reset (LazyValue)
518
- lazy_var = "@#{name}_lazy_value"
519
- if instance_variable_defined?(lazy_var)
520
- lazy_value = instance_variable_get(lazy_var)
521
- lazy_value&.reset!
522
- remove_instance_variable(lazy_var)
796
+ # Generate reset method that clears dependency resolution flag.
797
+ #
798
+ # Used by attributes with dependency caching to ensure dependencies
799
+ # are re-resolved after reset. Thread-safe operation that clears
800
+ # both computed state and dependency resolution state.
801
+ #
802
+ # @param name [Symbol] attribute name
803
+ # @return [void]
804
+ # @api private
805
+ def generate_reset_method_with_deps_flag(name)
806
+ computed_var = "@#{name}_computed"
807
+ value_var = "@#{name}_value"
808
+ exception_var = "@#{name}_exception"
809
+ deps_resolved_var = "@#{name}_deps_resolved"
810
+
811
+ define_method("reset_#{name}!") do
812
+ mutex = self.class.instance_variable_get(:@lazy_init_class_mutex)
813
+ mutex.synchronize do
814
+ remove_instance_variable(value_var) if instance_variable_defined?(value_var)
815
+ remove_instance_variable(exception_var) if instance_variable_defined?(exception_var)
816
+ instance_variable_set(computed_var, false)
817
+ instance_variable_set(deps_resolved_var, false) # Reset dependency flag
523
818
  end
524
819
  end
525
820
  end
526
821
 
527
- # Validate that the attribute name is suitable for method generation.
822
+ # Generate method with cached dependency resolution for multiple dependencies.
528
823
  #
529
- # Ensures the name follows Ruby method naming conventions and won't
530
- # cause issues when used to generate accessor methods.
824
+ # Optimizes attributes with multiple dependencies by caching the dependency
825
+ # resolution step. Once dependencies are resolved for an instance, subsequent
826
+ # calls skip the dependency resolver entirely.
531
827
  #
532
- # @param name [Object] the proposed attribute name
828
+ # @param name [Symbol] attribute name
829
+ # @param depends_on [Array<Symbol>] dependency attributes
830
+ # @param block [Proc] computation block
831
+ # @param config [Hash] attribute configuration
533
832
  # @return [void]
534
- # @raise [InvalidAttributeNameError] if the name is invalid
535
- def validate_attribute_name!(name)
536
- raise InvalidAttributeNameError, 'Attribute name cannot be nil' if name.nil?
537
- raise InvalidAttributeNameError, 'Attribute name cannot be empty' if name.to_s.strip.empty?
833
+ # @api private
834
+ def generate_fast_dependency_method(name, depends_on, block, config)
835
+ cached_block = block
836
+ cached_depends_on = depends_on
837
+ computed_var = "@#{name}_computed"
838
+ value_var = "@#{name}_value"
839
+ exception_var = "@#{name}_exception"
840
+ deps_resolved_var = "@#{name}_deps_resolved" # flag for resolved deps
538
841
 
539
- unless name.is_a?(Symbol) || name.is_a?(String)
540
- raise InvalidAttributeNameError, 'Attribute name must be a symbol or string'
541
- end
842
+ define_method(name) do
843
+ # Fast path: return cached result
844
+ return instance_variable_get(value_var) if instance_variable_get(computed_var)
542
845
 
543
- name_str = name.to_s
544
- return if name_str.match?(/\A[a-zA-Z_][a-zA-Z0-9_]*[?!]?\z/)
846
+ # Fast dependency check: skip resolution if already resolved
847
+ unless instance_variable_get(deps_resolved_var)
848
+ # Only resolve dependencies once
849
+ self.class.dependency_resolver.resolve_dependencies(name, self)
850
+ instance_variable_set(deps_resolved_var, true)
851
+ end
545
852
 
546
- raise InvalidAttributeNameError, "Invalid attribute name: #{name_str}"
853
+ # Thread-safe computation
854
+ mutex = self.class.instance_variable_get(:@lazy_init_class_mutex)
855
+ mutex.synchronize do
856
+ return instance_variable_get(value_var) if instance_variable_get(computed_var)
857
+
858
+ begin
859
+ result = instance_eval(&cached_block)
860
+ instance_variable_set(value_var, result)
861
+ instance_variable_set(computed_var, true)
862
+ result
863
+ rescue StandardError => e
864
+ instance_variable_set(exception_var, e)
865
+ instance_variable_set(computed_var, true)
866
+ raise
867
+ end
868
+ end
869
+ end
547
870
  end
548
871
  end
549
- end
872
+ end