77"""
88import logging
99from concurrent .futures import Future , ThreadPoolExecutor , wait , FIRST_COMPLETED
10+ from functools import partial
1011from typing import Callable
1112
1213from jumper_extension .adapters .ai_reviewer .benchmark import fingerprint
@@ -68,20 +69,30 @@ def __init__(
6869 self ._position : dict [str , int ] = {}
6970
7071 def run (self , baseline_code : str , variants : list [tuple [str , str ]]) -> dict :
71- """Benchmark *baseline_code*, then each ``(label, code)`` variant.
72-
73- Returns ``{label: BenchmarkResult}``; the baseline is under
74- ``BASELINE_LABEL``. An empty dict means the baseline itself would not
75- run, which leaves nothing to compare against.
72+ """Score each ``(label, code)`` variant the best the active checks allow.
73+
74+ With the timed run on, this measures *baseline_code* and every variant
75+ and compares their results, returning ``{label: BenchmarkResult}`` with
76+ the baseline under ``BASELINE_LABEL``. With the run turned off but
77+ ``validate_syntax`` still on, it falls back to a syntax-only pass that
78+ still repairs broken suggestions - just without timing or comparison,
79+ and without a baseline entry. With neither active there is nothing to
80+ do. An empty dict also means the baseline itself would not run, leaving
81+ nothing to compare against.
7682 """
77- if not self .checks .run .active :
78- # No timed replay: the only thing left worth measuring is gone, so
79- # there is nothing to compare. resolve_checks already warned why.
80- logger .info (
81- "[JUmPER]: benchmark timed run is off; nothing to measure or compare."
82- )
83- return {}
83+ if self .checks .run .active :
84+ return self ._run_timed (baseline_code , variants )
85+ if self .checks .validate_syntax .active :
86+ return self ._run_syntax_only (variants )
87+ # Both the run and the syntax gate are off: resolve_checks already
88+ # explained why, and there is genuinely nothing left to do here.
89+ logger .info (
90+ "[JUmPER]: benchmark has no active checks; nothing to measure or verify."
91+ )
92+ return {}
8493
94+ def _run_timed (self , baseline_code : str , variants : list [tuple [str , str ]]) -> dict :
95+ """The full benchmark: measure the baseline, then time and verify each variant."""
8596 self .progress = BenchmarkProgress (len (variants ), self .runs )
8697 self ._position = {label : index for index , (label , _ ) in enumerate (variants , start = 1 )}
8798
@@ -111,74 +122,63 @@ def run(self, baseline_code: str, variants: list[tuple[str, str]]) -> dict:
111122 timeout = self ._timeout_from (baseline_runs , duration )
112123
113124 results = {BASELINE_LABEL : baseline }
114- results .update (self ._benchmark_variants (variants , timeout , duration , prints ))
125+ results .update (
126+ self ._drive (
127+ variants ,
128+ partial (
129+ self ._process_timed ,
130+ timeout = timeout ,
131+ baseline_duration = duration ,
132+ baseline_prints = prints ,
133+ ),
134+ )
135+ )
115136 self .progress .finished ()
116137 return results
117138
118- def _benchmark_variants (
119- self ,
120- variants : list [tuple [str , str ]],
121- timeout : float ,
122- baseline_duration : float ,
123- baseline_prints : dict ,
124- ) -> dict :
139+ def _run_syntax_only (self , variants : list [tuple [str , str ]]) -> dict :
140+ """Check each suggestion parses, repairing what does not - no timing.
141+
142+ The timed run is off, so there is nothing to measure or fingerprint, but
143+ a broken suggestion can still be caught and handed to the same repair
144+ loop. Each surviving variant is reported OK with ``UNVERIFIED``
145+ correctness and its valid (possibly repaired) code in ``final_code``;
146+ ones that never parse are reported FAILED.
147+ """
148+ logger .info (
149+ f"[JUmPER]: benchmark: validating the syntax of { len (variants )} "
150+ "suggestion(s), with the timed run off."
151+ )
152+ self .progress = BenchmarkProgress (len (variants ), self .runs )
153+ self ._position = {label : index for index , (label , _ ) in enumerate (variants , start = 1 )}
154+
155+ results = self ._drive (variants , self ._process_syntax )
156+
157+ valid = sum (1 for result in results .values () if result .ok )
158+ logger .info (
159+ f"[JUmPER]: benchmark: syntax validated - { valid } valid, "
160+ f"{ len (results ) - valid } unfixable."
161+ )
162+ return results
163+
164+ def _drive (self , variants : list [tuple [str , str ]], process : Callable ) -> dict :
165+ """Run *process* over each variant, draining repairs the shared way.
166+
167+ Every mode differs only in what it does with a fresh candidate; the
168+ repair round-trip - fold a returned fix back into the queue, or settle a
169+ candidate whose repair produced nothing - is identical, so it lives here
170+ once. *process* is called as ``process(results, candidate, pending,
171+ repairing, pool)`` and is responsible for settling the candidate or
172+ submitting it for repair.
173+ """
125174 results : dict = {}
126175 pending = [_Candidate (label , code , self .fix_attempts ) for label , code in variants ]
127176 repairing : dict [Future , _Candidate ] = {}
128177
129178 with ThreadPoolExecutor (max_workers = max (1 , len (pending ))) as pool :
130179 while pending or repairing :
131180 if pending :
132- candidate = pending .pop (0 )
133- position = self ._position [candidate .label ]
134-
135- # Syntax is checkable for free, so broken code never costs a
136- # replay - whether it came from the model or from a repair.
137- if not self ._syntax_ok (candidate ):
138- self ._reject (results , candidate , position , pool , repairing )
139- continue
140-
141- outcome = self ._measure (
142- candidate .code ,
143- candidate .label ,
144- timeout ,
145- on_run = lambda index : self .progress .variant_run (position , index ),
146- )
147- outstanding = len (pending ) + len (repairing )
148- if isinstance (outcome , list ):
149- verdict = self ._verdict (
150- candidate , outcome , baseline_duration , baseline_prints
151- )
152- candidate .last_verdict = verdict
153- candidate .last_code = candidate .code
154-
155- # A wrong answer is worth repairing too: it costs the
156- # review more than a crash, since nothing else catches it.
157- if verdict .correctness == fingerprint .DIFFERS :
158- candidate .error = fingerprint .describe_divergence (
159- baseline_prints ,
160- outcome [- 1 ].fingerprints ,
161- verdict .differing_names ,
162- )
163- if self ._submit_fix (candidate , pool , repairing ):
164- self .progress .variant_diverged (
165- position ,
166- verdict .differing_names ,
167- candidate .attempts ,
168- self .fix_attempts ,
169- )
170- continue
171-
172- self ._settle (results , candidate , verdict )
173- self .progress .variant_done (
174- position ,
175- _summarize (verdict ),
176- _walls (outcome ),
177- outstanding = outstanding ,
178- )
179- else :
180- candidate .error = outcome .error
181- self ._reject (results , candidate , position , pool , repairing )
181+ process (results , pending .pop (0 ), pending , repairing , pool )
182182 continue
183183
184184 done , _ = wait (list (repairing ), return_when = FIRST_COMPLETED )
@@ -193,6 +193,89 @@ def _benchmark_variants(
193193 pending .append (candidate )
194194 return results
195195
196+ def _process_timed (
197+ self ,
198+ results : dict ,
199+ candidate : _Candidate ,
200+ pending : list ,
201+ repairing : dict ,
202+ pool : ThreadPoolExecutor ,
203+ * ,
204+ timeout : float ,
205+ baseline_duration : float ,
206+ baseline_prints : dict ,
207+ ) -> None :
208+ """Time one candidate against the baseline, repairing a crash or a divergence."""
209+ position = self ._position [candidate .label ]
210+
211+ # Syntax is checkable for free, so broken code never costs a replay -
212+ # whether it came from the model or from a repair.
213+ if not self ._syntax_ok (candidate ):
214+ self ._reject (results , candidate , position , pool , repairing )
215+ return
216+
217+ outcome = self ._measure (
218+ candidate .code ,
219+ candidate .label ,
220+ timeout ,
221+ on_run = lambda index : self .progress .variant_run (position , index ),
222+ )
223+ outstanding = len (pending ) + len (repairing )
224+ if isinstance (outcome , list ):
225+ verdict = self ._verdict (candidate , outcome , baseline_duration , baseline_prints )
226+ candidate .last_verdict = verdict
227+ candidate .last_code = candidate .code
228+
229+ # A wrong answer is worth repairing too: it costs the review more
230+ # than a crash, since nothing else catches it.
231+ if verdict .correctness == fingerprint .DIFFERS :
232+ candidate .error = fingerprint .describe_divergence (
233+ baseline_prints ,
234+ outcome [- 1 ].fingerprints ,
235+ verdict .differing_names ,
236+ )
237+ if self ._submit_fix (candidate , pool , repairing ):
238+ self .progress .variant_diverged (
239+ position ,
240+ verdict .differing_names ,
241+ candidate .attempts ,
242+ self .fix_attempts ,
243+ )
244+ return
245+
246+ self ._settle (results , candidate , verdict )
247+ self .progress .variant_done (
248+ position ,
249+ _summarize (verdict ),
250+ _walls (outcome ),
251+ outstanding = outstanding ,
252+ )
253+ else :
254+ candidate .error = outcome .error
255+ self ._reject (results , candidate , position , pool , repairing )
256+
257+ def _process_syntax (
258+ self ,
259+ results : dict ,
260+ candidate : _Candidate ,
261+ pending : list ,
262+ repairing : dict ,
263+ pool : ThreadPoolExecutor ,
264+ ) -> None :
265+ """Parse one candidate, repairing it if it does not - never running it."""
266+ position = self ._position [candidate .label ]
267+ if self ._syntax_ok (candidate ):
268+ self .final_code [candidate .label ] = candidate .code
269+ results [candidate .label ] = BenchmarkResult (
270+ label = candidate .label ,
271+ status = OK ,
272+ attempts = candidate .attempts ,
273+ correctness = fingerprint .UNVERIFIED ,
274+ )
275+ self .progress .variant_validated (position , candidate .attempts )
276+ else :
277+ self ._reject (results , candidate , position , pool , repairing )
278+
196279 def _reject (
197280 self ,
198281 results : dict ,
0 commit comments