hone 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.
Files changed (105) hide show
  1. checksums.yaml +7 -0
  2. data/.standard.yml +8 -0
  3. data/CHANGELOG.md +5 -0
  4. data/LICENSE.txt +21 -0
  5. data/README.md +201 -0
  6. data/Rakefile +10 -0
  7. data/examples/.hone/harness.rb +41 -0
  8. data/examples/README.md +22 -0
  9. data/examples/allocation_patterns.rb +66 -0
  10. data/examples/cpu_patterns.rb +50 -0
  11. data/examples/jit_patterns.rb +69 -0
  12. data/exe/hone +7 -0
  13. data/lib/hone/adapters/base.rb +35 -0
  14. data/lib/hone/adapters/fasterer.rb +38 -0
  15. data/lib/hone/adapters/rubocop_performance.rb +85 -0
  16. data/lib/hone/analyzer.rb +258 -0
  17. data/lib/hone/cli.rb +247 -0
  18. data/lib/hone/config.rb +93 -0
  19. data/lib/hone/correlator.rb +250 -0
  20. data/lib/hone/exit_codes.rb +10 -0
  21. data/lib/hone/finding.rb +64 -0
  22. data/lib/hone/finding_filter.rb +57 -0
  23. data/lib/hone/formatters/base.rb +25 -0
  24. data/lib/hone/formatters/filterable.rb +31 -0
  25. data/lib/hone/formatters/github.rb +71 -0
  26. data/lib/hone/formatters/json.rb +75 -0
  27. data/lib/hone/formatters/junit.rb +154 -0
  28. data/lib/hone/formatters/sarif.rb +179 -0
  29. data/lib/hone/formatters/tsv.rb +49 -0
  30. data/lib/hone/harness.rb +57 -0
  31. data/lib/hone/harness_generator.rb +128 -0
  32. data/lib/hone/harness_runner.rb +172 -0
  33. data/lib/hone/method_map.rb +140 -0
  34. data/lib/hone/patterns/README.md +174 -0
  35. data/lib/hone/patterns/array_compact.rb +105 -0
  36. data/lib/hone/patterns/array_include_set.rb +34 -0
  37. data/lib/hone/patterns/base.rb +90 -0
  38. data/lib/hone/patterns/block_to_proc.rb +109 -0
  39. data/lib/hone/patterns/bsearch_vs_find.rb +80 -0
  40. data/lib/hone/patterns/chars_map_ord.rb +42 -0
  41. data/lib/hone/patterns/chars_to_variable.rb +136 -0
  42. data/lib/hone/patterns/chars_to_variable_tainted.rb +136 -0
  43. data/lib/hone/patterns/constant_regexp.rb +74 -0
  44. data/lib/hone/patterns/count_vs_size.rb +35 -0
  45. data/lib/hone/patterns/divmod.rb +92 -0
  46. data/lib/hone/patterns/dynamic_ivar.rb +44 -0
  47. data/lib/hone/patterns/dynamic_ivar_get.rb +33 -0
  48. data/lib/hone/patterns/each_with_index.rb +116 -0
  49. data/lib/hone/patterns/each_with_object.rb +63 -0
  50. data/lib/hone/patterns/flatten_once.rb +28 -0
  51. data/lib/hone/patterns/gsub_to_tr.rb +48 -0
  52. data/lib/hone/patterns/hash_each_key.rb +41 -0
  53. data/lib/hone/patterns/hash_each_value.rb +31 -0
  54. data/lib/hone/patterns/hash_keys_include.rb +30 -0
  55. data/lib/hone/patterns/hash_merge_bang.rb +33 -0
  56. data/lib/hone/patterns/hash_values_include.rb +31 -0
  57. data/lib/hone/patterns/inject_sum.rb +48 -0
  58. data/lib/hone/patterns/kernel_loop.rb +27 -0
  59. data/lib/hone/patterns/lazy_ivar.rb +39 -0
  60. data/lib/hone/patterns/map_compact.rb +32 -0
  61. data/lib/hone/patterns/map_flatten.rb +31 -0
  62. data/lib/hone/patterns/map_select_chain.rb +32 -0
  63. data/lib/hone/patterns/parallel_assignment.rb +127 -0
  64. data/lib/hone/patterns/positive_predicate.rb +27 -0
  65. data/lib/hone/patterns/range_include.rb +34 -0
  66. data/lib/hone/patterns/redundant_string_chars.rb +82 -0
  67. data/lib/hone/patterns/regexp_match.rb +126 -0
  68. data/lib/hone/patterns/reverse_each.rb +30 -0
  69. data/lib/hone/patterns/reverse_first.rb +40 -0
  70. data/lib/hone/patterns/select_count.rb +32 -0
  71. data/lib/hone/patterns/select_first.rb +31 -0
  72. data/lib/hone/patterns/select_map.rb +32 -0
  73. data/lib/hone/patterns/shuffle_first.rb +30 -0
  74. data/lib/hone/patterns/slice_with_length.rb +48 -0
  75. data/lib/hone/patterns/sort_by_first.rb +31 -0
  76. data/lib/hone/patterns/sort_by_last.rb +31 -0
  77. data/lib/hone/patterns/sort_first.rb +52 -0
  78. data/lib/hone/patterns/sort_last.rb +30 -0
  79. data/lib/hone/patterns/sort_reverse.rb +53 -0
  80. data/lib/hone/patterns/string_casecmp.rb +54 -0
  81. data/lib/hone/patterns/string_chars_each.rb +56 -0
  82. data/lib/hone/patterns/string_concat_in_loop.rb +116 -0
  83. data/lib/hone/patterns/string_delete_prefix.rb +53 -0
  84. data/lib/hone/patterns/string_delete_suffix.rb +53 -0
  85. data/lib/hone/patterns/string_empty.rb +64 -0
  86. data/lib/hone/patterns/string_end_with.rb +81 -0
  87. data/lib/hone/patterns/string_shovel.rb +75 -0
  88. data/lib/hone/patterns/string_start_with.rb +80 -0
  89. data/lib/hone/patterns/taint_tracking_base.rb +230 -0
  90. data/lib/hone/patterns/times_map.rb +38 -0
  91. data/lib/hone/patterns/uniq_by.rb +32 -0
  92. data/lib/hone/patterns/yield_vs_block.rb +72 -0
  93. data/lib/hone/profilers/base.rb +162 -0
  94. data/lib/hone/profilers/factory.rb +31 -0
  95. data/lib/hone/profilers/memory_profiler.rb +213 -0
  96. data/lib/hone/profilers/stackprof.rb +99 -0
  97. data/lib/hone/profilers/vernier.rb +147 -0
  98. data/lib/hone/reporter.rb +371 -0
  99. data/lib/hone/scanner.rb +75 -0
  100. data/lib/hone/suggestion_generator.rb +23 -0
  101. data/lib/hone/version.rb +5 -0
  102. data/lib/hone.rb +108 -0
  103. data/logo.png +0 -0
  104. data/sig/hone.rbs +4 -0
  105. metadata +176 -0
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Hone
4
+ module Patterns
5
+ # Pattern: instance_variable_set("@#{name}", value)
6
+ #
7
+ # Dynamic instance variable creation causes object shape transitions.
8
+ # YJIT optimizes based on stable object shapes - when shapes change,
9
+ # it must deoptimize. Use a Hash for dynamic data instead.
10
+ #
11
+ # Impact: Significant with YJIT, none without
12
+ class DynamicIvar < Base
13
+ self.pattern_id = :dynamic_ivar
14
+ self.optimization_type = :jit
15
+
16
+ def visit_call_node(node)
17
+ super
18
+
19
+ return unless node.name == :instance_variable_set
20
+
21
+ first_arg = node.arguments&.arguments&.first
22
+ return unless first_arg
23
+ return unless dynamic_ivar_name?(first_arg)
24
+
25
+ add_finding(
26
+ node,
27
+ message: "Dynamic instance_variable_set causes object shape transitions, hurting YJIT. Use a Hash for dynamic data instead.",
28
+ speedup: "Enables better YJIT optimization"
29
+ )
30
+ end
31
+
32
+ private
33
+
34
+ def dynamic_ivar_name?(node)
35
+ # Interpolated string like "@#{name}"
36
+ node.is_a?(Prism::InterpolatedStringNode) ||
37
+ # String concatenation
38
+ (node.is_a?(Prism::CallNode) && node.name == :+) ||
39
+ # Variable (not a literal symbol/string)
40
+ node.is_a?(Prism::LocalVariableReadNode)
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Hone
4
+ module Patterns
5
+ # Pattern: instance_variable_get("@#{name}")
6
+ #
7
+ # Often paired with dynamic instance_variable_set. Suggests the code
8
+ # is using ivars as a dynamic key-value store, which hurts JIT.
9
+ #
10
+ # Impact: Significant with YJIT, none without
11
+ class DynamicIvarGet < Base
12
+ self.pattern_id = :dynamic_ivar_get
13
+ self.optimization_type = :jit
14
+
15
+ def visit_call_node(node)
16
+ super
17
+
18
+ return unless node.name == :instance_variable_get
19
+
20
+ first_arg = node.arguments&.arguments&.first
21
+ return unless first_arg
22
+ return unless first_arg.is_a?(Prism::InterpolatedStringNode) ||
23
+ first_arg.is_a?(Prism::LocalVariableReadNode)
24
+
25
+ add_finding(
26
+ node,
27
+ message: "Dynamic instance_variable_get suggests ivars used as key-value store. Use a Hash instead for better YJIT performance.",
28
+ speedup: "Significant with YJIT, none without"
29
+ )
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,116 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "set"
4
+
5
+ module Hone
6
+ module Patterns
7
+ # Pattern: array.each_with_index { |item, i| ... } when index or element unused
8
+ #
9
+ # When using each_with_index but only using the element (not the index),
10
+ # use plain .each instead to avoid index tracking overhead.
11
+ #
12
+ # When using each_with_index but only using the index (not the element),
13
+ # use array.size.times { |i| ... } instead for clarity and efficiency.
14
+ #
15
+ # Examples:
16
+ # # Bad: index `i` is never used
17
+ # array.each_with_index { |item, i| puts item }
18
+ # # Good: use plain each
19
+ # array.each { |item| puts item }
20
+ #
21
+ # # Bad: element `item` is never used
22
+ # array.each_with_index { |item, i| puts i }
23
+ # # Good: use times
24
+ # array.size.times { |i| puts i }
25
+ class EachWithIndex < Base
26
+ self.pattern_id = :each_with_index
27
+ self.optimization_type = :cpu
28
+
29
+ def visit_call_node(node)
30
+ super
31
+ return unless node.name == :each_with_index && node.block.is_a?(Prism::BlockNode)
32
+
33
+ check_block_parameter_usage(node)
34
+ end
35
+
36
+ private
37
+
38
+ def check_block_parameter_usage(node)
39
+ block = node.block
40
+ parameters = block.parameters
41
+
42
+ # If no parameters or not the expected structure, skip
43
+ return unless parameters.is_a?(Prism::BlockParametersNode)
44
+ return unless parameters.parameters.is_a?(Prism::ParametersNode)
45
+
46
+ params = parameters.parameters
47
+ requireds = params.requireds
48
+
49
+ # We expect two required parameters for each_with_index: |element, index|
50
+ return unless requireds.size == 2
51
+
52
+ element_param = requireds[0]
53
+ index_param = requireds[1]
54
+
55
+ # Get parameter names
56
+ element_name = extract_param_name(element_param)
57
+ index_name = extract_param_name(index_param)
58
+
59
+ return unless element_name && index_name
60
+
61
+ # Collect all local variable reads in the block body
62
+ used_locals = collect_local_variable_reads(block.body)
63
+
64
+ element_used = used_locals.include?(element_name)
65
+ index_used = used_locals.include?(index_name)
66
+
67
+ if !index_used && element_used
68
+ add_finding(
69
+ node,
70
+ message: "Index parameter `#{index_name}` is not used. Use `.each` instead of `.each_with_index` to avoid index tracking overhead.",
71
+ speedup: "Minor, avoids index tracking overhead"
72
+ )
73
+ elsif !element_used && index_used
74
+ add_finding(
75
+ node,
76
+ message: "Element parameter `#{element_name}` is not used. Consider using `.size.times { |#{index_name}| }` instead of `.each_with_index` for clarity.",
77
+ speedup: "Minor, avoids index tracking overhead"
78
+ )
79
+ elsif !element_used && !index_used
80
+ add_finding(
81
+ node,
82
+ message: "Neither element nor index parameters are used. Consider using `.size.times` or `.each` depending on intent.",
83
+ speedup: "Minor, avoids index tracking overhead"
84
+ )
85
+ end
86
+ end
87
+
88
+ def extract_param_name(param)
89
+ case param
90
+ when Prism::RequiredParameterNode
91
+ param.name
92
+ end
93
+ end
94
+
95
+ def collect_local_variable_reads(node)
96
+ collector = LocalVariableCollector.new
97
+ node&.accept(collector)
98
+ collector.used_names
99
+ end
100
+
101
+ # Helper visitor to collect all local variable read references
102
+ class LocalVariableCollector < Prism::Visitor
103
+ attr_reader :used_names
104
+
105
+ def initialize
106
+ @used_names = Set.new
107
+ end
108
+
109
+ def visit_local_variable_read_node(node)
110
+ @used_names << node.name
111
+ super
112
+ end
113
+ end
114
+ end
115
+ end
116
+ end
@@ -0,0 +1,63 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Hone
4
+ module Patterns
5
+ # Pattern: inject({}) { |acc, x| ... ; acc } -> each_with_object({}) { |x, acc| }
6
+ #
7
+ # inject/reduce with a hash or array accumulator requires returning the
8
+ # accumulator at the end of each block iteration. each_with_object
9
+ # automatically passes the same object, making the code cleaner and
10
+ # avoiding the need to return the accumulator.
11
+ #
12
+ # Examples:
13
+ # # Bad: must return acc at end of block
14
+ # items.inject({}) { |acc, x| acc[x.id] = x; acc }
15
+ # # Good: acc is automatically passed
16
+ # items.each_with_object({}) { |x, acc| acc[x.id] = x }
17
+ class EachWithObject < Base
18
+ self.pattern_id = :each_with_object
19
+ self.optimization_type = :allocation
20
+
21
+ def visit_call_node(node)
22
+ super
23
+
24
+ return unless %i[inject reduce].include?(node.name)
25
+ return unless block_attached?(node)
26
+ return unless empty_collection_initial_value?(node)
27
+
28
+ add_finding(
29
+ node,
30
+ message: "Consider using `.each_with_object(#{initial_value_literal(node)})` instead of `.#{node.name}(#{initial_value_literal(node)})` for cleaner accumulator pattern",
31
+ speedup: "Cleaner, avoids returning accumulator each iteration"
32
+ )
33
+ end
34
+
35
+ private
36
+
37
+ def empty_collection_initial_value?(node)
38
+ args = node.arguments&.arguments
39
+ return false unless args&.size == 1
40
+
41
+ arg = args.first
42
+ empty_hash?(arg) || empty_array?(arg)
43
+ end
44
+
45
+ def empty_hash?(arg)
46
+ arg.is_a?(Prism::HashNode) && arg.elements.empty?
47
+ end
48
+
49
+ def empty_array?(arg)
50
+ arg.is_a?(Prism::ArrayNode) && arg.elements.empty?
51
+ end
52
+
53
+ def initial_value_literal(node)
54
+ arg = node.arguments.arguments.first
55
+ if arg.is_a?(Prism::HashNode)
56
+ "{}"
57
+ else
58
+ "[]"
59
+ end
60
+ end
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Hone
4
+ module Patterns
5
+ # Pattern: array.flatten -> consider array.flatten(1)
6
+ #
7
+ # flatten without an argument flattens all levels recursively.
8
+ # If you only need one level, flatten(1) is faster.
9
+ class FlattenOnce < Base
10
+ self.pattern_id = :flatten_once
11
+ self.optimization_type = :cpu
12
+
13
+ def visit_call_node(node)
14
+ super
15
+
16
+ # Look for: .flatten without arguments
17
+ return unless node.name == :flatten
18
+ return unless node.arguments.nil?
19
+
20
+ add_finding(
21
+ node,
22
+ message: "Consider `.flatten(1)` if only one level of flattening is needed",
23
+ speedup: "Faster when you only need one level"
24
+ )
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Hone
4
+ module Patterns
5
+ # Pattern: str.gsub('a', 'b') -> str.tr('a', 'b')
6
+ #
7
+ # For replacing single characters, tr is optimized and faster than gsub.
8
+ # gsub has regex overhead even for simple string patterns.
9
+ class GsubToTr < Base
10
+ self.pattern_id = :gsub_to_tr
11
+ self.optimization_type = :cpu
12
+
13
+ def visit_call_node(node)
14
+ super
15
+
16
+ return unless node.name == :gsub
17
+
18
+ args = node.arguments&.arguments
19
+ return unless args&.size == 2
20
+
21
+ first_arg = args[0]
22
+ second_arg = args[1]
23
+
24
+ # Check if both arguments are single-character strings
25
+ return unless single_char_string?(first_arg) && single_char_string?(second_arg)
26
+
27
+ add_finding(
28
+ node,
29
+ message: "Use `.tr('#{string_value(first_arg)}', '#{string_value(second_arg)}')` instead of `.gsub` for single character replacement",
30
+ speedup: "tr is optimized for character translation, avoids regex overhead"
31
+ )
32
+ end
33
+
34
+ private
35
+
36
+ def single_char_string?(node)
37
+ return false unless node.is_a?(Prism::StringNode)
38
+
39
+ content = node.content
40
+ content.is_a?(String) && content.length == 1
41
+ end
42
+
43
+ def string_value(node)
44
+ node.content
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Hone
4
+ module Patterns
5
+ # Pattern: hash.keys.each { } -> hash.each_key { }
6
+ #
7
+ # keys.each creates an intermediate array of keys then iterates.
8
+ # each_key iterates keys directly without allocation.
9
+ #
10
+ # Also detects hash.values.each { } -> hash.each_value { }
11
+ class HashEachKey < Base
12
+ self.pattern_id = :hash_each_key
13
+ self.optimization_type = :allocation
14
+
15
+ def visit_call_node(node)
16
+ super
17
+
18
+ # Look for: .each { } where receiver is .keys or .values
19
+ return unless node.name == :each && block_attached?(node)
20
+
21
+ receiver = node.receiver
22
+ return unless receiver.is_a?(Prism::CallNode)
23
+
24
+ case receiver.name
25
+ when :keys
26
+ add_finding(
27
+ node,
28
+ message: "Use `.each_key { }` instead of `.keys.each { }` to avoid intermediate array",
29
+ speedup: "Iterates keys directly without allocating array"
30
+ )
31
+ when :values
32
+ add_finding(
33
+ node,
34
+ message: "Use `.each_value { }` instead of `.values.each { }` to avoid intermediate array",
35
+ speedup: "Iterates values directly without allocating array"
36
+ )
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Hone
4
+ module Patterns
5
+ # Pattern: hash.values.each { } -> hash.each_value { }
6
+ #
7
+ # values.each creates an intermediate array of values then iterates.
8
+ # each_value iterates values directly without allocation.
9
+ class HashEachValue < Base
10
+ self.pattern_id = :hash_each_value
11
+ self.optimization_type = :allocation
12
+
13
+ def visit_call_node(node)
14
+ super
15
+
16
+ # Look for: .each { } where receiver is .values
17
+ return unless node.name == :each && block_attached?(node)
18
+
19
+ receiver = node.receiver
20
+ return unless receiver.is_a?(Prism::CallNode)
21
+ return unless receiver.name == :values
22
+
23
+ add_finding(
24
+ node,
25
+ message: "Use `.each_value { }` instead of `.values.each { }` to avoid intermediate array",
26
+ speedup: "Avoids creating intermediate array of values"
27
+ )
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Hone
4
+ module Patterns
5
+ # Pattern: hash.keys.include?(key) -> hash.key?(key)
6
+ #
7
+ # keys.include? creates an array of all keys then searches it (O(n)).
8
+ # key? does a direct hash lookup (O(1)).
9
+ class HashKeysInclude < Base
10
+ self.pattern_id = :hash_keys_include
11
+ self.optimization_type = :allocation
12
+
13
+ def visit_call_node(node)
14
+ super
15
+
16
+ # Look for: .include?(x) where receiver is .keys
17
+ return unless node.name == :include? && node.arguments&.arguments&.size == 1
18
+
19
+ receiver = node.receiver
20
+ return unless receiver.is_a?(Prism::CallNode) && receiver.name == :keys
21
+
22
+ add_finding(
23
+ node,
24
+ message: "Use `.key?(k)` instead of `.keys.include?(k)` for O(1) lookup without array allocation",
25
+ speedup: "O(n) to O(1), avoids allocating array of all keys"
26
+ )
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Hone
4
+ module Patterns
5
+ # Pattern: hash = hash.merge(other) -> hash.merge!(other)
6
+ #
7
+ # Reassigning to the same variable after merge creates a new hash.
8
+ # merge! mutates in place, avoiding allocation.
9
+ class HashMergeBang < Base
10
+ self.pattern_id = :hash_merge_bang
11
+ self.optimization_type = :allocation
12
+
13
+ def visit_local_variable_write_node(node)
14
+ super
15
+
16
+ # Look for: x = x.merge(...)
17
+ value = node.value
18
+ return unless value.is_a?(Prism::CallNode)
19
+ return unless value.name == :merge
20
+
21
+ receiver = value.receiver
22
+ return unless receiver.is_a?(Prism::LocalVariableReadNode)
23
+ return unless receiver.name == node.name
24
+
25
+ add_finding(
26
+ node,
27
+ message: "Use `.merge!` instead of `#{node.name} = #{node.name}.merge(...)` to avoid creating new hash",
28
+ speedup: "Avoids creating new hash, mutates in place"
29
+ )
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Hone
4
+ module Patterns
5
+ # Pattern: hash.values.include?(x) -> hash.value?(x)
6
+ #
7
+ # values.include? creates an intermediate array of values then searches.
8
+ # value? checks directly without allocation.
9
+ class HashValuesInclude < Base
10
+ self.pattern_id = :hash_values_include
11
+ self.optimization_type = :allocation
12
+
13
+ def visit_call_node(node)
14
+ super
15
+
16
+ # Look for: .include?(arg) where receiver is .values
17
+ return unless node.name == :include? && node.arguments&.arguments&.size == 1
18
+
19
+ receiver = node.receiver
20
+ return unless receiver.is_a?(Prism::CallNode)
21
+ return unless receiver.name == :values
22
+
23
+ add_finding(
24
+ node,
25
+ message: "Use `.value?(x)` instead of `.values.include?(x)` to avoid intermediate array",
26
+ speedup: "Avoids creating intermediate array of values"
27
+ )
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Hone
4
+ module Patterns
5
+ # Pattern: array.inject(0, :+) -> array.sum
6
+ #
7
+ # sum is optimized for numeric summation with native implementation.
8
+ # Also handles empty arrays correctly (returns 0).
9
+ class InjectSum < Base
10
+ self.pattern_id = :inject_sum
11
+ self.optimization_type = :cpu
12
+
13
+ def visit_call_node(node)
14
+ super
15
+
16
+ return unless %i[inject reduce].include?(node.name)
17
+ return unless sum_pattern?(node)
18
+
19
+ add_finding(
20
+ node,
21
+ message: "Use `.sum` instead of `.#{node.name}(:+)` for optimized numeric summation",
22
+ speedup: "Native C implementation, cleaner code"
23
+ )
24
+ end
25
+
26
+ private
27
+
28
+ def sum_pattern?(node)
29
+ args = node.arguments&.arguments
30
+ return false unless args
31
+
32
+ # Pattern 1: inject(:+) or reduce(:+)
33
+ if args.size == 1
34
+ arg = args.first
35
+ return true if arg.is_a?(Prism::SymbolNode) && arg.value == "+"
36
+ end
37
+
38
+ # Pattern 2: inject(0, :+) or reduce(0, :+)
39
+ if args.size == 2
40
+ second_arg = args[1]
41
+ return true if second_arg.is_a?(Prism::SymbolNode) && second_arg.value == "+"
42
+ end
43
+
44
+ false
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Hone
4
+ module Patterns
5
+ # Pattern: Kernel#loop { break if ... } -> while true ... end
6
+ #
7
+ # Kernel#loop has method call overhead vs the while construct.
8
+ #
9
+ # From sqids-ruby commit 8a74142
10
+ class KernelLoop < Base
11
+ self.pattern_id = :kernel_loop
12
+ self.optimization_type = :cpu
13
+
14
+ def visit_call_node(node)
15
+ super
16
+ # Look for: loop { ... } (implicit receiver, block present)
17
+ return unless node.name == :loop && node.receiver.nil? && node.block.is_a?(Prism::BlockNode)
18
+
19
+ add_finding(
20
+ node,
21
+ message: "Use `while true ... end` instead of `loop { }` in hot paths to avoid method call overhead",
22
+ speedup: "Minor, but adds up in tight loops"
23
+ )
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Hone
4
+ module Patterns
5
+ # Pattern: @ivar ||= value (outside initialize)
6
+ #
7
+ # When an ivar is first assigned outside initialize, it changes the
8
+ # object's shape at runtime. YJIT prefers stable shapes from creation.
9
+ #
10
+ # This pattern detects ||= with ivars outside of initialize methods.
11
+ #
12
+ # Impact: Moderate with YJIT, none without
13
+ class LazyIvar < Base
14
+ self.pattern_id = :lazy_ivar
15
+ self.optimization_type = :jit
16
+
17
+ def initialize(file_path)
18
+ super
19
+ @in_initialize = false
20
+ end
21
+
22
+ def visit_def_node(node)
23
+ with_context(:@in_initialize, node.name == :initialize) { super }
24
+ end
25
+
26
+ def visit_instance_variable_or_write_node(node)
27
+ super
28
+ # @ivar ||= value pattern
29
+ return if @in_initialize
30
+
31
+ add_finding(
32
+ node,
33
+ message: "Lazy ivar initialization (||=) outside initialize causes shape transitions. Define #{node.name} = nil in initialize for better YJIT.",
34
+ speedup: "Moderate with YJIT, none without"
35
+ )
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Hone
4
+ module Patterns
5
+ # Pattern: array.map { }.compact -> array.filter_map { }
6
+ #
7
+ # map.compact creates an intermediate array from map that may contain nils,
8
+ # then compacts it. filter_map combines both operations avoiding the
9
+ # intermediate allocation.
10
+ class MapCompact < Base
11
+ self.pattern_id = :map_compact
12
+ self.optimization_type = :allocation
13
+
14
+ def visit_call_node(node)
15
+ super
16
+
17
+ # Look for: .compact where receiver is .map with a block
18
+ return unless node.name == :compact
19
+
20
+ receiver = node.receiver
21
+ return unless receiver.is_a?(Prism::CallNode)
22
+ return unless receiver.name == :map && block_attached?(receiver)
23
+
24
+ add_finding(
25
+ node,
26
+ message: "Use `.filter_map { }` instead of `.map { }.compact` to avoid intermediate array with nils",
27
+ speedup: "Avoids intermediate array with nils"
28
+ )
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Hone
4
+ module Patterns
5
+ # Pattern: array.map { }.flatten -> array.flat_map { }
6
+ #
7
+ # map.flatten creates an intermediate array from map, then flattens it.
8
+ # flat_map combines both operations avoiding the intermediate allocation.
9
+ class MapFlatten < Base
10
+ self.pattern_id = :map_flatten
11
+ self.optimization_type = :allocation
12
+
13
+ def visit_call_node(node)
14
+ super
15
+
16
+ # Look for: .flatten where receiver is .map with a block
17
+ return unless node.name == :flatten
18
+
19
+ receiver = node.receiver
20
+ return unless receiver.is_a?(Prism::CallNode)
21
+ return unless receiver.name == :map && block_attached?(receiver)
22
+
23
+ add_finding(
24
+ node,
25
+ message: "Use `.flat_map { }` instead of `.map { }.flatten` to avoid intermediate array",
26
+ speedup: "Single pass, no intermediate allocation"
27
+ )
28
+ end
29
+ end
30
+ end
31
+ end