cataract 0.1.3 → 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.
- checksums.yaml +4 -4
- data/.github/workflows/ci-manual-rubies.yml +44 -0
- data/.overcommit.yml +1 -1
- data/.rubocop.yml +96 -4
- data/.rubocop_todo.yml +186 -0
- data/BENCHMARKS.md +62 -141
- data/CHANGELOG.md +20 -0
- data/RAGEL_MIGRATION.md +2 -2
- data/README.md +37 -4
- data/Rakefile +72 -32
- data/cataract.gemspec +4 -1
- data/ext/cataract/cataract.c +59 -50
- data/ext/cataract/cataract.h +5 -3
- data/ext/cataract/css_parser.c +173 -65
- data/ext/cataract/extconf.rb +2 -2
- data/ext/cataract/{merge.c → flatten.c} +526 -468
- data/ext/cataract/shorthand_expander.c +164 -115
- data/lib/cataract/at_rule.rb +8 -9
- data/lib/cataract/declaration.rb +18 -0
- data/lib/cataract/import_resolver.rb +63 -43
- data/lib/cataract/import_statement.rb +49 -0
- data/lib/cataract/pure/byte_constants.rb +69 -0
- data/lib/cataract/pure/flatten.rb +1145 -0
- data/lib/cataract/pure/helpers.rb +35 -0
- data/lib/cataract/pure/imports.rb +268 -0
- data/lib/cataract/pure/parser.rb +1340 -0
- data/lib/cataract/pure/serializer.rb +590 -0
- data/lib/cataract/pure/specificity.rb +206 -0
- data/lib/cataract/pure.rb +153 -0
- data/lib/cataract/rule.rb +69 -15
- data/lib/cataract/stylesheet.rb +356 -49
- data/lib/cataract/version.rb +1 -1
- data/lib/cataract.rb +43 -26
- metadata +14 -26
- data/benchmarks/benchmark_harness.rb +0 -193
- data/benchmarks/benchmark_merging.rb +0 -121
- data/benchmarks/benchmark_optimization_comparison.rb +0 -168
- data/benchmarks/benchmark_parsing.rb +0 -153
- data/benchmarks/benchmark_ragel_removal.rb +0 -56
- data/benchmarks/benchmark_runner.rb +0 -70
- data/benchmarks/benchmark_serialization.rb +0 -180
- data/benchmarks/benchmark_shorthand.rb +0 -109
- data/benchmarks/benchmark_shorthand_expansion.rb +0 -176
- data/benchmarks/benchmark_specificity.rb +0 -124
- data/benchmarks/benchmark_string_allocation.rb +0 -151
- data/benchmarks/benchmark_stylesheet_to_s.rb +0 -62
- data/benchmarks/benchmark_to_s_cached.rb +0 -55
- data/benchmarks/benchmark_value_splitter.rb +0 -54
- data/benchmarks/benchmark_yjit.rb +0 -158
- data/benchmarks/benchmark_yjit_workers.rb +0 -61
- data/benchmarks/profile_to_s.rb +0 -23
- data/benchmarks/speedup_calculator.rb +0 -83
- data/benchmarks/system_metadata.rb +0 -81
- data/benchmarks/templates/benchmarks.md.erb +0 -221
- data/benchmarks/yjit_tests.rb +0 -141
- data/scripts/fuzzer/run.rb +0 -828
- data/scripts/fuzzer/worker.rb +0 -99
- data/scripts/generate_benchmarks_md.rb +0 -155
data/scripts/fuzzer/worker.rb
DELETED
|
@@ -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:
|