cataract 0.1.3 → 0.1.4

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 (53) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci-manual-rubies.yml +27 -0
  3. data/.overcommit.yml +1 -1
  4. data/.rubocop.yml +62 -0
  5. data/.rubocop_todo.yml +186 -0
  6. data/BENCHMARKS.md +60 -139
  7. data/CHANGELOG.md +10 -0
  8. data/README.md +30 -2
  9. data/Rakefile +49 -22
  10. data/cataract.gemspec +4 -1
  11. data/ext/cataract/cataract.c +47 -47
  12. data/ext/cataract/css_parser.c +17 -33
  13. data/ext/cataract/merge.c +6 -0
  14. data/lib/cataract/at_rule.rb +8 -9
  15. data/lib/cataract/declaration.rb +18 -0
  16. data/lib/cataract/import_resolver.rb +3 -4
  17. data/lib/cataract/pure/byte_constants.rb +69 -0
  18. data/lib/cataract/pure/helpers.rb +35 -0
  19. data/lib/cataract/pure/imports.rb +255 -0
  20. data/lib/cataract/pure/merge.rb +1146 -0
  21. data/lib/cataract/pure/parser.rb +1236 -0
  22. data/lib/cataract/pure/serializer.rb +590 -0
  23. data/lib/cataract/pure/specificity.rb +206 -0
  24. data/lib/cataract/pure.rb +130 -0
  25. data/lib/cataract/rule.rb +22 -13
  26. data/lib/cataract/stylesheet.rb +14 -9
  27. data/lib/cataract/version.rb +1 -1
  28. data/lib/cataract.rb +18 -5
  29. metadata +12 -25
  30. data/benchmarks/benchmark_harness.rb +0 -193
  31. data/benchmarks/benchmark_merging.rb +0 -121
  32. data/benchmarks/benchmark_optimization_comparison.rb +0 -168
  33. data/benchmarks/benchmark_parsing.rb +0 -153
  34. data/benchmarks/benchmark_ragel_removal.rb +0 -56
  35. data/benchmarks/benchmark_runner.rb +0 -70
  36. data/benchmarks/benchmark_serialization.rb +0 -180
  37. data/benchmarks/benchmark_shorthand.rb +0 -109
  38. data/benchmarks/benchmark_shorthand_expansion.rb +0 -176
  39. data/benchmarks/benchmark_specificity.rb +0 -124
  40. data/benchmarks/benchmark_string_allocation.rb +0 -151
  41. data/benchmarks/benchmark_stylesheet_to_s.rb +0 -62
  42. data/benchmarks/benchmark_to_s_cached.rb +0 -55
  43. data/benchmarks/benchmark_value_splitter.rb +0 -54
  44. data/benchmarks/benchmark_yjit.rb +0 -158
  45. data/benchmarks/benchmark_yjit_workers.rb +0 -61
  46. data/benchmarks/profile_to_s.rb +0 -23
  47. data/benchmarks/speedup_calculator.rb +0 -83
  48. data/benchmarks/system_metadata.rb +0 -81
  49. data/benchmarks/templates/benchmarks.md.erb +0 -221
  50. data/benchmarks/yjit_tests.rb +0 -141
  51. data/scripts/fuzzer/run.rb +0 -828
  52. data/scripts/fuzzer/worker.rb +0 -99
  53. data/scripts/generate_benchmarks_md.rb +0 -155
@@ -1,99 +0,0 @@
1
- #!/usr/bin/env ruby
2
- # frozen_string_literal: true
3
-
4
- # Fuzzer worker process - runs in subprocess and parses CSS inputs from stdin
5
- # Communicates via length-prefixed protocol
6
-
7
- require 'cataract'
8
- require 'cataract/color_conversion'
9
-
10
- # Configure aggressive GC to help identify memory leaks
11
- # Disable auto_compact - it can cause issues with C extensions holding pointers
12
- GC.auto_compact = false
13
- GC.config(
14
- malloc_limit: 1_000_000,
15
- malloc_limit_growth_factor: 1.1, # Grow very slowly
16
- oldmalloc_limit_growth_factor: 1.1 # Grow very slowly
17
- )
18
-
19
- # Enable GC.stress mode if requested (VERY slow, but makes GC bugs reproducible)
20
- if ENV['FUZZ_GC_STRESS'] == '1'
21
- GC.stress = true
22
- warn '[Worker] GC.stress enabled - expect 100-1000x slowdown'
23
- end
24
-
25
- COLOR_FORMATS = %i[hex rgb hsl hwb oklab oklch lab lch].freeze
26
-
27
- # Read length-prefixed inputs and parse them
28
- loop do
29
- # Read 4-byte length prefix (network byte order)
30
- len_bytes = $stdin.read(4)
31
- break if len_bytes.nil? || len_bytes.bytesize != 4
32
-
33
- length = len_bytes.unpack1('N')
34
-
35
- # Read CSS input
36
- css = $stdin.read(length)
37
- break if css.nil? || css.bytesize != length
38
-
39
- # Parse CSS (crash will kill subprocess)
40
- begin
41
- stylesheet = Cataract.parse_css(css)
42
- rules = stylesheet.rules.to_a
43
- merge_tested = false
44
- to_s_tested = false
45
- color_converted = false
46
-
47
- # Test merge with valid CSS followed by fuzzed CSS
48
- # This tests merge error handling when second rule set is invalid
49
- begin
50
- valid_stylesheet = Cataract.parse_css('body { margin: 0; color: red; }')
51
- valid_stylesheet.add_block(css) # Add fuzzed CSS to valid stylesheet
52
- valid_stylesheet.merge # Call merge on the stylesheet
53
- merge_tested = true
54
- rescue Cataract::Error
55
- # Expected - merge might fail on invalid CSS
56
- end
57
-
58
- # Test to_s on parsed rules occasionally
59
- # This tests serialization on fuzzed data
60
- if !rules.empty? && rand < 0.01
61
- stylesheet.to_s
62
- to_s_tested = true
63
- end
64
-
65
- if !rules.empty? && rand < 0.02
66
- stylesheet.to_formatted_s
67
- to_s_tested = true
68
- end
69
-
70
- # Test color conversion occasionally (10% chance)
71
- # This tests color parsing/conversion on fuzzed color values
72
- if rand < 0.1
73
- begin
74
- # Try random color format conversions
75
- from_format = COLOR_FORMATS.sample
76
- to_format = COLOR_FORMATS.sample
77
- stylesheet.convert_colors!(from: from_format, to: to_format)
78
- color_converted = true
79
- rescue Cataract::Error, ArgumentError
80
- # Expected - color conversion might fail on invalid colors
81
- end
82
- end
83
-
84
- # Report what was tested: PARSE [+MERGE] [+TOS] [+COLOR]
85
- output = 'PARSE'
86
- output += '+MERGE' if merge_tested
87
- output += '+TOS' if to_s_tested
88
- output += '+COLOR' if color_converted
89
- $stdout.write("#{output}\n")
90
- rescue Cataract::DepthError
91
- $stdout.write("DEPTH\n")
92
- rescue Cataract::SizeError
93
- $stdout.write("SIZE\n")
94
- rescue StandardError
95
- $stdout.write("ERR\n")
96
- end
97
-
98
- $stdout.flush
99
- end
@@ -1,155 +0,0 @@
1
- #!/usr/bin/env ruby
2
- # frozen_string_literal: true
3
-
4
- require 'json'
5
- require 'erb'
6
- require 'fileutils'
7
-
8
- # Generate BENCHMARKS.md from benchmark JSON results
9
- class BenchmarkDocGenerator
10
- RESULTS_DIR = File.expand_path('../benchmarks/.results', __dir__)
11
- TEMPLATE_PATH = File.expand_path('../benchmarks/templates/benchmarks.md.erb', __dir__)
12
- OUTPUT_PATH = File.expand_path('../BENCHMARKS.md', __dir__)
13
-
14
- def initialize(results_dir: RESULTS_DIR, output_path: OUTPUT_PATH, verbose: true)
15
- @results_dir = results_dir
16
- @output_path = output_path
17
- @verbose = verbose
18
- @metadata = load_metadata
19
- @parsing_data = load_benchmark_data('parsing')
20
- @serialization_data = load_benchmark_data('serialization')
21
- @specificity_data = load_benchmark_data('specificity')
22
- @merging_data = load_benchmark_data('merging')
23
- @yjit_data = load_benchmark_data('yjit')
24
- end
25
-
26
- def generate
27
- # Check if we have any data to generate
28
- if !@parsing_data && !@serialization_data &&
29
- !@specificity_data && !@merging_data && !@yjit_data
30
- # :nocov:
31
- if @verbose
32
- puts '⚠ Warning: No benchmark data found. Run benchmarks first: rake benchmark'
33
- puts 'Available data files:'
34
- Dir.glob(File.join(@results_dir, '*.json')).each do |file|
35
- puts " - #{File.basename(file)}"
36
- end
37
- end
38
- # :nocov:
39
- return
40
- end
41
-
42
- template = ERB.new(File.read(TEMPLATE_PATH), trim_mode: '-')
43
- output = template.result(binding)
44
-
45
- File.write(@output_path, output)
46
-
47
- return unless @verbose
48
-
49
- # :nocov:
50
- puts '✓ Generated BENCHMARKS.md'
51
- puts ' Included benchmarks:'
52
- puts ' - Parsing' if @parsing_data
53
- puts ' - Serialization' if @serialization_data
54
- puts ' - Specificity' if @specificity_data
55
- puts ' - Merging' if @merging_data
56
- puts ' - YJIT' if @yjit_data
57
-
58
- missing = []
59
- missing << 'Parsing' unless @parsing_data
60
- missing << 'Serialization' unless @serialization_data
61
- missing << 'Specificity' unless @specificity_data
62
- missing << 'Merging' unless @merging_data
63
- missing << 'YJIT' unless @yjit_data
64
-
65
- return unless missing.any?
66
-
67
- puts ' Missing benchmarks:'
68
- missing.each { |name| puts " - #{name}" }
69
- # :nocov:
70
- end
71
-
72
- private
73
-
74
- def load_metadata
75
- metadata_path = File.join(@results_dir, 'metadata.json')
76
- if File.exist?(metadata_path)
77
- JSON.parse(File.read(metadata_path))
78
- else
79
- # :nocov:
80
- warn '⚠ Warning: metadata.json not found. Run benchmarks first.'
81
- {}
82
- # :nocov:
83
- end
84
- end
85
-
86
- def load_benchmark_data(name)
87
- path = File.join(@results_dir, "#{name}.json")
88
- return nil unless File.exist?(path)
89
-
90
- JSON.parse(File.read(path))
91
- rescue JSON::ParserError => e
92
- # :nocov:
93
- warn "⚠ Warning: Failed to parse #{name}.json: #{e.message}"
94
- nil
95
- # :nocov:
96
- end
97
-
98
- # Formatting helpers for ERB template
99
-
100
- def format_ips(result, short: false)
101
- ips = result['central_tendency']
102
-
103
- formatted = if ips >= 1_000_000
104
- "#{(ips / 1_000_000.0).round(2)}M"
105
- elsif ips >= 1_000
106
- "#{(ips / 1_000.0).round(2)}K"
107
- else
108
- ips.round(1).to_s
109
- end
110
-
111
- if short
112
- "#{formatted} i/s"
113
- else
114
- time_per_op = format_time_per_op(result)
115
- "#{formatted} i/s (#{time_per_op})"
116
- end
117
- end
118
-
119
- def format_time_per_op(result)
120
- ips = result['central_tendency']
121
- time_us = 1_000_000.0 / ips
122
-
123
- if time_us >= 1_000
124
- "#{(time_us / 1_000).round(2)} ms"
125
- else
126
- "#{time_us.round(2)} μs"
127
- end
128
- end
129
-
130
- def format_speedup(speedup)
131
- "#{speedup.round(2)}x faster"
132
- end
133
-
134
- def format_number(num)
135
- num.to_s.reverse.gsub(/(\d{3})(?=\d)/, '\\1,').reverse
136
- end
137
-
138
- # Access instance variables for ERB
139
- attr_reader :metadata, :parsing_data, :serialization_data,
140
- :specificity_data, :merging_data, :yjit_data
141
- end
142
-
143
- # Run if called directly
144
- # :nocov:
145
- if __FILE__ == $PROGRAM_NAME
146
- unless Dir.exist?(BenchmarkDocGenerator::RESULTS_DIR)
147
- puts "Error: No benchmark results found at #{BenchmarkDocGenerator::RESULTS_DIR}"
148
- puts 'Run benchmarks first: rake benchmark'
149
- exit 1
150
- end
151
-
152
- generator = BenchmarkDocGenerator.new
153
- generator.generate
154
- end
155
- # :nocov: