lrama-fuzz 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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 20f3eb480a5c1390a4bbf41c9fe786d5a2c4d5a6f71d1131a9db21962c58706a
4
+ data.tar.gz: a8276babf5102702fdc250718298c5f2840ebd2f05691c3a97c98d596ba4252c
5
+ SHA512:
6
+ metadata.gz: 79b4593ed0821e4e1102e426170132be2de49206e0b12301101e9e374f40be11731c6f937378401bcd1321c047d95791bcc73fe4cea2f8211910ef238aa841fc
7
+ data.tar.gz: 13a57d3e084070b2d7f557c73a4e4b7ed90290dc16ab66160c2f2145a3a6b77bb1cd6d9ddef8ef054768c0762e212af50d1a7f2bf8c887bd664d0d3b1743f521
data/Gemfile ADDED
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gemspec
6
+
7
+ gem "prism"
8
+ gem "racc"
9
+ gem "rake"
10
+ gem "test-unit"
data/LICENSE ADDED
@@ -0,0 +1,7 @@
1
+ Copyright 2026-present, Kevin Newton
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
4
+
5
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
6
+
7
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,228 @@
1
+ # lrama-fuzz
2
+
3
+ Grammar-based fuzzer for [lrama](https://github.com/ruby/lrama) grammars. Generates random strings from any lrama grammar, with built-in profiles for fuzzing Ruby (via Prism or RubyVM) and JSON.
4
+
5
+ ## Installation
6
+
7
+ In your Gemfile:
8
+
9
+ ```ruby
10
+ gem "lrama-fuzz"
11
+ ```
12
+
13
+ Or install directly:
14
+
15
+ ```sh
16
+ gem install lrama-fuzz
17
+ ```
18
+
19
+ ## Quick start
20
+
21
+ ### Ruby fuzzing (Prism)
22
+
23
+ ```ruby
24
+ require "lrama/fuzz"
25
+
26
+ session = Lrama::Fuzz.prism(ruby_src_dir: "/path/to/ruby", seed: 42)
27
+
28
+ # Generate a raw grammar derivation (may or may not be valid Ruby)
29
+ puts session.generate
30
+
31
+ # Generate a composed, valid Ruby program
32
+ puts session.compose
33
+
34
+ # Evolve programs over 10 generations, optimizing for complexity
35
+ best = session.evolve(10, population_size: 50)
36
+ best.each { |code, fitness| puts "#{fitness.round(2)}: #{code}" }
37
+
38
+ # Check grammar rule coverage
39
+ session.generate_full_coverage(max_attempts: 500)
40
+ cov = session.coverage
41
+ puts "#{cov.covered_count}/#{cov.total_count} rules (#{(cov.ratio * 100).round(1)}%)"
42
+ ```
43
+
44
+ ### Ruby fuzzing (RubyVM)
45
+
46
+ Uses `RubyVM::InstructionSequence.compile_parsey` for validation instead of Prism. This tests the lrama-generated parser directly.
47
+
48
+ ```ruby
49
+ session = Lrama::Fuzz.rubyvm(ruby_src_dir: "/path/to/ruby", seed: 42)
50
+ puts session.compose
51
+ ```
52
+
53
+ ### JSON fuzzing
54
+
55
+ Uses the JSON grammar from `examples/json.y` -- no external files needed.
56
+
57
+ ```ruby
58
+ session = Lrama::Fuzz.json(seed: 42)
59
+
60
+ puts session.generate # raw derivation
61
+ puts session.generate_valid(max_retries: 50) # valid JSON document
62
+ ```
63
+
64
+ ## Coverage-guided generation
65
+
66
+ Generate programs with validity feedback. The generator tracks which grammar rules have appeared in valid programs and biases future generation toward rules that haven't been tested in valid contexts yet.
67
+
68
+ ```ruby
69
+ session = Lrama::Fuzz.json(seed: 42)
70
+
71
+ # Generate 200 programs with automatic feedback
72
+ valid_programs = session.generate_guided(count: 200)
73
+ puts "#{valid_programs.size} valid out of 200"
74
+
75
+ # Check valid coverage (rules seen in valid programs)
76
+ cov = session.coverage
77
+ puts "Raw coverage: #{(cov.ratio * 100).round(1)}%"
78
+ puts "Valid coverage: #{(cov.valid_ratio * 100).round(1)}%"
79
+
80
+ # Use the block form for per-program handling
81
+ session.generate_guided(count: 100) do |code, valid|
82
+ File.write("corpus/#{Time.now.to_f}.json", code) if valid
83
+ end
84
+ ```
85
+
86
+ The generator also uses valid coverage in its rule selection: after all rules have been expanded at least once, it prefers rules that haven't yet appeared in any valid program. This drives generation toward under-tested parts of the grammar.
87
+
88
+ ## Shrinking
89
+
90
+ Minimize a failing input to the smallest version that still triggers the bug. Uses delta debugging (line-level, then character-level).
91
+
92
+ ```ruby
93
+ # Standalone
94
+ small = Lrama::Fuzz::Shrinker.shrink(big_program) { |code| crashes?(code) }
95
+
96
+ # Via session
97
+ small = session.shrink(big_program) { |code| crashes?(code) }
98
+ ```
99
+
100
+ ## CLI
101
+
102
+ ```
103
+ $ lrama-fuzz --help
104
+ Usage: lrama-fuzz [options]
105
+
106
+ Generates programs from lrama grammars.
107
+
108
+ --profile PROFILE Profile: prism, rubyvm, json (default: prism)
109
+ -d, --ruby-src-dir DIR Path to Ruby source (default: $RUBY_SRC_DIR)
110
+ --grammar-path PATH Path to grammar file (json only)
111
+ -n, --count N Number of programs to generate (default: 10)
112
+ -m, --mode MODE Mode: generate, compose, evolve, coverage (default: generate)
113
+ -g, --generations N Generations for evolve mode (default: 10)
114
+ -p, --population N Population size for evolve mode (default: 50)
115
+ -s, --seed N Random seed for reproducibility
116
+ -h, --help Show this help
117
+ ```
118
+
119
+ Examples:
120
+
121
+ ```sh
122
+ # Generate 5 composed Ruby programs
123
+ RUBY_SRC_DIR=/path/to/ruby lrama-fuzz -m compose -n 5
124
+
125
+ # Generate valid JSON
126
+ lrama-fuzz --profile json -m generate -n 10
127
+
128
+ # Evolve Ruby programs for 20 generations
129
+ RUBY_SRC_DIR=/path/to/ruby lrama-fuzz -m evolve -g 20 -p 30
130
+
131
+ # Measure grammar rule coverage
132
+ RUBY_SRC_DIR=/path/to/ruby lrama-fuzz -m coverage -n 500
133
+ ```
134
+
135
+ ## Custom grammars
136
+
137
+ You can fuzz any lrama grammar by using the core API directly.
138
+
139
+ ```ruby
140
+ require "lrama/fuzz"
141
+
142
+ # Parse a grammar
143
+ grammar = Lrama::Fuzz.parse("path/to/grammar.y")
144
+
145
+ # Define terminal generators -- each token name maps to a string or proc
146
+ terminals = {
147
+ "NUMBER" => -> { rand(1..100).to_s },
148
+ "STRING" => -> { %w[foo bar baz].sample }
149
+ }
150
+
151
+ # Create a generator
152
+ generator = Lrama::Fuzz::Generator.new(
153
+ grammar,
154
+ terminals: terminals,
155
+ max_depth: 10, # depth limit for derivation (default: 10)
156
+ random: Random.new(42)
157
+ )
158
+
159
+ # Generate strings
160
+ 10.times { puts generator.generate }
161
+
162
+ # Generate strings that pass a validator
163
+ valid = generator.generate_valid(max_retries: 100) { |s| valid?(s) }
164
+
165
+ # Track coverage
166
+ generator.generate_full_coverage(max_attempts: 500)
167
+ puts generator.coverage.ratio
168
+ ```
169
+
170
+ ### Wrapping in a Session
171
+
172
+ For access to composition, evolution, and shrinking, wrap a generator in a `Session`:
173
+
174
+ ```ruby
175
+ session = Lrama::Fuzz::Session.new(
176
+ generator,
177
+ fitness: ->(code) { code.length > 10 ? 1.5 : 0.3 },
178
+ validator: ->(code) { code.length > 0 },
179
+ random: Random.new(42)
180
+ )
181
+
182
+ session.generate # raw derivation
183
+ session.generate_valid # passes validator
184
+ session.evolve(10, population_size: 20) # evolutionary optimization
185
+ session.shrink(code) { |c| some_predicate?(c) } # delta debugging
186
+ ```
187
+
188
+ ## Architecture
189
+
190
+ ```
191
+ Lrama::Fuzz
192
+ .prism(ruby_src_dir:, seed:) # -> Session (Prism profile)
193
+ .rubyvm(ruby_src_dir:, seed:) # -> Session (RubyVM profile)
194
+ .json(seed:) # -> Session (JSON profile)
195
+ .parse(path) # -> Grammar
196
+ .new(path, terminals:, **opts) # -> Generator
197
+
198
+ Session # unified interface
199
+ #generate # raw grammar derivation
200
+ #generate_valid # derivation that passes validator
201
+ #generate_guided # generate with validity feedback loop
202
+ #compose # template-composed valid program (Ruby only)
203
+ #evolve(n) # evolutionary optimization
204
+ #shrink(code, &pred) # delta debugging minimizer
205
+ #coverage # grammar rule coverage tracker
206
+
207
+ Generator # core derivation engine
208
+ #generate # random derivation from start symbol
209
+ #generate_valid # retry until validator passes
210
+ #record_result # feed back validity for coverage guidance
211
+ #generate_full_coverage # target uncovered rules
212
+
213
+ Coverage # grammar rule coverage tracking
214
+ #ratio # raw coverage (rules expanded / reachable)
215
+ #valid_ratio # valid coverage (rules in valid programs / reachable)
216
+ #uncovered_valid_rules # rules not yet seen in valid programs
217
+
218
+ Profiles (provide fitness, validator, session factory):
219
+ Prism # validates with ::Prism.parse
220
+ RubyVM # validates with ::RubyVM::InstructionSequence.compile_parsey
221
+ Json # validates with JSON.parse
222
+
223
+ Ruby # shared Ruby grammar infrastructure (classifier, composer)
224
+ Shrinker # delta debugging minimizer
225
+ Joiner # token spacing/joining
226
+ Evolver # genome-based evolutionary optimization
227
+ ComposedEvolver # evolutionary optimization with composer
228
+ ```
data/Rakefile ADDED
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "rake/testtask"
5
+
6
+ Rake::TestTask.new(:test) do |t|
7
+ t.libs << "test"
8
+ t.test_files = FileList["test/**/*_test.rb"]
9
+ end
10
+
11
+ task default: :test
@@ -0,0 +1,9 @@
1
+ %token NUMBER
2
+
3
+ %%
4
+
5
+ expr: NUMBER
6
+ | expr '+' expr
7
+ | expr '*' expr
8
+ | '(' expr ')'
9
+ ;
data/examples/json.y ADDED
@@ -0,0 +1,31 @@
1
+ %token STRING NUMBER TRUE FALSE NULL
2
+
3
+ %%
4
+
5
+ value: object
6
+ | array
7
+ | STRING
8
+ | NUMBER
9
+ | TRUE
10
+ | FALSE
11
+ | NULL
12
+ ;
13
+
14
+ object: '{' '}'
15
+ | '{' members '}'
16
+ ;
17
+
18
+ members: pair
19
+ | members ',' pair
20
+ ;
21
+
22
+ pair: STRING ':' value
23
+ ;
24
+
25
+ array: '[' ']'
26
+ | '[' elements ']'
27
+ ;
28
+
29
+ elements: value
30
+ | elements ',' value
31
+ ;
data/examples/lists.y ADDED
@@ -0,0 +1,14 @@
1
+ %token ITEM SEPARATOR
2
+
3
+ %%
4
+
5
+ program: list
6
+ ;
7
+
8
+ list: %empty
9
+ | list_items
10
+ ;
11
+
12
+ list_items: ITEM
13
+ | list_items SEPARATOR ITEM
14
+ ;
data/exe/lrama-fuzz ADDED
@@ -0,0 +1,170 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require "bundler/setup"
5
+ require "optparse"
6
+ require "lrama/fuzz"
7
+
8
+ module Lrama
9
+ module Fuzz
10
+ module CLI
11
+ def self.run(argv = ARGV)
12
+ options = {
13
+ mode: :generate,
14
+ count: 10,
15
+ generations: 10,
16
+ population_size: 50,
17
+ seed: nil,
18
+ profile: :prism,
19
+ ruby_src_dir: ENV["RUBY_SRC_DIR"],
20
+ grammar_path: nil
21
+ }
22
+
23
+ parser = ::OptionParser.new do |opts|
24
+ opts.banner = "Usage: lrama-fuzz [options]"
25
+ opts.separator ""
26
+ opts.separator "Generates programs from lrama grammars."
27
+ opts.separator ""
28
+
29
+ opts.on("--profile PROFILE", %i[prism rubyvm json],
30
+ "Profile: prism, rubyvm, json (default: prism)") do |p|
31
+ options[:profile] = p
32
+ end
33
+
34
+ opts.on("-d", "--ruby-src-dir DIR", "Path to Ruby source (default: $RUBY_SRC_DIR)") do |dir|
35
+ options[:ruby_src_dir] = dir
36
+ end
37
+
38
+ opts.on("--grammar-path PATH", "Path to grammar file (json only)") do |path|
39
+ options[:grammar_path] = path
40
+ end
41
+
42
+ opts.on("-n", "--count N", Integer, "Number of programs to generate (default: 10)") do |n|
43
+ options[:count] = n
44
+ end
45
+
46
+ opts.on("-m", "--mode MODE", %i[generate compose evolve coverage guided],
47
+ "Mode: generate, compose, evolve, coverage, guided (default: generate)") do |mode|
48
+ options[:mode] = mode
49
+ end
50
+
51
+ opts.on("-g", "--generations N", Integer, "Generations for evolve mode (default: 10)") do |n|
52
+ options[:generations] = n
53
+ end
54
+
55
+ opts.on("-p", "--population N", Integer, "Population size for evolve mode (default: 50)") do |n|
56
+ options[:population_size] = n
57
+ end
58
+
59
+ opts.on("-s", "--seed N", Integer, "Random seed for reproducibility") do |n|
60
+ options[:seed] = n
61
+ end
62
+
63
+ opts.on("-h", "--help", "Show this help") do
64
+ puts opts
65
+ exit
66
+ end
67
+ end
68
+
69
+ parser.parse!(argv)
70
+ session = build_session(options)
71
+
72
+ case options[:mode]
73
+ when :generate
74
+ emit(options[:count]) { session.generate }
75
+ when :compose
76
+ emit(options[:count]) { session.compose }
77
+ when :evolve
78
+ run_evolve(session, options)
79
+ when :coverage
80
+ run_coverage(session, options)
81
+ when :guided
82
+ run_guided(session, options)
83
+ end
84
+ end
85
+
86
+ def self.build_session(options)
87
+ case options[:profile]
88
+ when :prism, :rubyvm
89
+ unless options[:ruby_src_dir]
90
+ $stderr.puts "Error: Ruby source directory required."
91
+ $stderr.puts "Set RUBY_SRC_DIR or pass --ruby-src-dir."
92
+ exit 1
93
+ end
94
+
95
+ unless File.exist?(File.join(options[:ruby_src_dir], "parse.y"))
96
+ $stderr.puts "Error: #{options[:ruby_src_dir]}/parse.y not found."
97
+ exit 1
98
+ end
99
+
100
+ $stderr.puts "Loading Ruby grammar (#{options[:profile]})..."
101
+ session = Fuzz.public_send(options[:profile],
102
+ ruby_src_dir: options[:ruby_src_dir],
103
+ seed: options[:seed])
104
+ $stderr.puts "Ready."
105
+ session
106
+ when :json
107
+ $stderr.puts "Loading JSON grammar..."
108
+ kwargs = { seed: options[:seed] }
109
+ kwargs[:grammar_path] = options[:grammar_path] if options[:grammar_path]
110
+ session = Fuzz.json(**kwargs)
111
+ $stderr.puts "Ready."
112
+ session
113
+ end
114
+ end
115
+
116
+ def self.emit(count)
117
+ count.times do
118
+ puts yield
119
+ puts "---"
120
+ end
121
+ end
122
+
123
+ def self.run_evolve(session, options)
124
+ $stderr.puts "Evolving #{options[:population_size]} programs for #{options[:generations]} generations..."
125
+ best = session.evolve(
126
+ options[:generations],
127
+ population_size: options[:population_size]
128
+ )
129
+
130
+ best.each do |code, fitness|
131
+ puts "# fitness: #{fitness.round(4)}"
132
+ puts code
133
+ puts "---"
134
+ end
135
+
136
+ $stderr.puts "Best fitness: #{best.first[1].round(4)}"
137
+ end
138
+
139
+ def self.run_coverage(session, options)
140
+ $stderr.puts "Generating programs to measure coverage..."
141
+ session.generate_full_coverage(max_attempts: options[:count])
142
+
143
+ cov = session.coverage
144
+ $stderr.puts "Coverage: #{cov.covered_count}/#{cov.total_count} rules (#{(cov.ratio * 100).round(1)}%)"
145
+ end
146
+
147
+ def self.run_guided(session, options)
148
+ $stderr.puts "Generating #{options[:count]} programs with validity feedback..."
149
+ valid_count = 0
150
+
151
+ session.generate_guided(count: options[:count]) do |code, valid|
152
+ if valid
153
+ valid_count += 1
154
+ puts code
155
+ puts "---"
156
+ end
157
+ end
158
+
159
+ cov = session.coverage
160
+ $stderr.puts "Valid: #{valid_count}/#{options[:count]}"
161
+ $stderr.puts "Raw coverage: #{cov.covered_count}/#{cov.total_count} (#{(cov.ratio * 100).round(1)}%)"
162
+ $stderr.puts "Valid coverage: #{(cov.valid_ratio * 100).round(1)}%"
163
+ end
164
+
165
+ private_class_method :build_session
166
+ end
167
+ end
168
+ end
169
+
170
+ Lrama::Fuzz::CLI.run
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lrama
4
+ module Fuzz
5
+ # A Random-compatible object that reads values from a codon (integer)
6
+ # sequence. This allows deterministic replay and evolution of any code
7
+ # path that uses Random — in particular, the Composer's template and
8
+ # fragment selection.
9
+ #
10
+ # Wraps around when the sequence is exhausted, so any genome length
11
+ # can drive arbitrarily many decisions.
12
+ class CodonRandom
13
+ def initialize(codons)
14
+ @codons = codons
15
+ @index = 0
16
+ end
17
+
18
+ # Compatible with Random#rand:
19
+ # rand() -> Float in [0, 1)
20
+ # rand(n) -> Integer in [0, n)
21
+ # rand(a..b) -> Integer in [a, b]
22
+ def rand(max = nil)
23
+ codon = next_codon
24
+
25
+ case max
26
+ when nil
27
+ codon.to_f / GeneticOperators::MAX_CODON
28
+ when Integer
29
+ max == 0 ? 0 : codon % max
30
+ when Range
31
+ min_val = max.min
32
+ span = max.max - min_val + (max.exclude_end? ? 0 : 1)
33
+ span <= 0 ? min_val : min_val + (codon % span)
34
+ else
35
+ raise ArgumentError, "unexpected argument: #{max.inspect}"
36
+ end
37
+ end
38
+
39
+ private
40
+
41
+ def next_codon
42
+ codon = @codons[@index % @codons.size]
43
+ @index += 1
44
+ codon
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,94 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lrama
4
+ module Fuzz
5
+ # Evolves genomes that drive Composer template and fragment selection
6
+ # via CodonRandom. Unlike the grammar-level Evolver, every genome here
7
+ # produces a structurally valid program (the Composer enforces that),
8
+ # so evolution optimizes for fitness (complexity, diversity) rather
9
+ # than basic validity.
10
+ #
11
+ # Usage:
12
+ # generator = Generator.new(grammar, terminals: ..., ...)
13
+ # evolver = ComposedEvolver.new(generator, fitness: Ruby.fitness)
14
+ # results = evolver.evolve # => [[code, fitness], ...]
15
+ class ComposedEvolver
16
+ include GeneticOperators
17
+
18
+ DEFAULT_POPULATION_SIZE = 50
19
+ DEFAULT_GENOME_LENGTH = 300
20
+ DEFAULT_MUTATION_RATE = 0.05
21
+
22
+ attr_reader :generation, :best_fitness
23
+
24
+ def initialize(generator, fitness:, composer_class: Ruby::Composer, validator: nil, population_size: DEFAULT_POPULATION_SIZE, genome_length: DEFAULT_GENOME_LENGTH, mutation_rate: DEFAULT_MUTATION_RATE, random: Random.new)
25
+ @generator = generator
26
+ @fitness = fitness
27
+ @composer_class = composer_class
28
+ @validator = validator
29
+ @population_size = population_size
30
+ @genome_length = genome_length
31
+ @mutation_rate = mutation_rate
32
+ @random = random
33
+ @generation = 0
34
+ @best_fitness = 0.0
35
+ @last_evaluated = nil
36
+
37
+ @population = Array.new(population_size) { random_genome }
38
+ end
39
+
40
+ # Run one generation: evaluate, select, crossover, mutate.
41
+ # Returns an array of [code, fitness] pairs.
42
+ def evolve
43
+ evaluated = @population.map do |genome|
44
+ code = generate_from_genome(genome)
45
+ score = @fitness.call(code)
46
+ Individual.new(genome: genome, code: code, fitness: score)
47
+ end
48
+
49
+ evaluated.sort_by! { |ind| -ind.fitness }
50
+ @best_fitness = evaluated.first.fitness
51
+
52
+ new_population = []
53
+
54
+ # Elitism: keep top 20%
55
+ elite_count = [@population_size / 5, 1].max
56
+ new_population.concat(evaluated.first(elite_count).map(&:genome))
57
+
58
+ # Fill rest with crossover + mutation
59
+ while new_population.size < @population_size
60
+ p1 = tournament_select(evaluated)
61
+ p2 = tournament_select(evaluated)
62
+ child = crossover(p1, p2)
63
+ mutate!(child)
64
+ new_population << child
65
+ end
66
+
67
+ @population = new_population.first(@population_size)
68
+ @last_evaluated = evaluated
69
+ @generation += 1
70
+
71
+ evaluated.map { |ind| [ind.code, ind.fitness] }
72
+ end
73
+
74
+ # Generate a program from a genome by driving the Composer with
75
+ # a CodonRandom seeded from the genome.
76
+ def generate_from_genome(genome)
77
+ codon_random = CodonRandom.new(genome)
78
+ composer = @composer_class.new(@generator, random: codon_random, validator: @validator)
79
+ composer.generate
80
+ end
81
+
82
+ # Return the best programs from the current population.
83
+ def best_programs(n = 10)
84
+ evaluated = @last_evaluated || @population.map do |genome|
85
+ code = generate_from_genome(genome)
86
+ score = @fitness.call(code)
87
+ Individual.new(genome: genome, code: code, fitness: score)
88
+ end
89
+
90
+ evaluated.sort_by { |ind| -ind.fitness }.first(n).map { |ind| [ind.code, ind.fitness] }
91
+ end
92
+ end
93
+ end
94
+ end