@@ -159,20 +159,34 @@ def _stability_pass(max_dev: float, ref_scale: float, floor: float) -> bool:
159159)
160160
161161
162- def _read_source_line (fname : str , lineno : int ) -> str :
163- """Return the raw source line at lineno (1-based), or '' if unavailable."""
162+ def _resolve_source (fname : str , search_whole_tree : bool = False ) -> str :
163+ """Resolve a (possibly bare) source filename to an existing path, or '' if not
164+ found. An absolute existing path is used as-is; otherwise the basename is
165+ located recursively under src/ (then the whole tree if `search_whole_tree`)."""
164166 if os .path .isabs (fname ) and os .path .isfile (fname ):
165- candidates = [fname ]
166- else :
167- candidates = glob .glob (os .path .join (MFC_ROOT_DIR , "src" , "**" , os .path .basename (fname )), recursive = True )
168- if not candidates :
169- return ""
167+ return fname
168+ candidates = glob .glob (os .path .join (MFC_ROOT_DIR , "src" , "**" , os .path .basename (fname )), recursive = True )
169+ if not candidates and search_whole_tree :
170+ candidates = glob .glob (os .path .join (MFC_ROOT_DIR , "**" , os .path .basename (fname )), recursive = True )
171+ return candidates [0 ] if candidates else ""
172+
173+
174+ def _read_source_lines (fname : str , search_whole_tree : bool = False ) -> list :
175+ """Resolve `fname` and return its lines (with newlines), or [] if unreadable."""
176+ path = _resolve_source (fname , search_whole_tree )
177+ if not path :
178+ return []
170179 try :
171- with open (candidates [0 ]) as fh :
172- lines = fh .readlines ()
173- return lines [lineno - 1 ] if 0 < lineno <= len (lines ) else ""
180+ with open (path ) as fh :
181+ return fh .readlines ()
174182 except OSError :
175- return ""
183+ return []
184+
185+
186+ def _read_source_line (fname : str , lineno : int ) -> str :
187+ """Return the raw source line at lineno (1-based), or '' if unavailable."""
188+ lines = _read_source_lines (fname )
189+ return lines [lineno - 1 ] if 0 < lineno <= len (lines ) else ""
176190
177191
178192def _macro_context_in_lines (lines : list , lineno : int ) -> str :
@@ -199,16 +213,8 @@ def _macro_context_in_lines(lines: list, lineno: int) -> str:
199213
200214def _macro_context (fname : str , lineno : int ) -> str :
201215 """File-backed wrapper around _macro_context_in_lines; '' path safe."""
202- if os .path .isabs (fname ) and os .path .isfile (fname ):
203- candidates = [fname ]
204- else :
205- candidates = glob .glob (os .path .join (MFC_ROOT_DIR , "src" , "**" , os .path .basename (fname )), recursive = True )
206- if not candidates :
207- return None
208- try :
209- with open (candidates [0 ]) as fh :
210- lines = fh .readlines ()
211- except OSError :
216+ lines = _read_source_lines (fname )
217+ if not lines :
212218 return None
213219 return _macro_context_in_lines (lines , lineno )
214220
@@ -241,17 +247,7 @@ def _statement_bounds_in_lines(lines: list, lineno: int) -> tuple:
241247def _statement_at (fname : str , lineno : int ) -> tuple :
242248 """File-backed (start, end, text) for the logical statement at fname:lineno;
243249 text is the joined statement. Returns (lineno, lineno, '') if unreadable."""
244- if os .path .isabs (fname ) and os .path .isfile (fname ):
245- candidates = [fname ]
246- else :
247- candidates = glob .glob (os .path .join (MFC_ROOT_DIR , "src" , "**" , os .path .basename (fname )), recursive = True )
248- if not candidates :
249- return lineno , lineno , ""
250- try :
251- with open (candidates [0 ]) as fh :
252- lines = fh .readlines ()
253- except OSError :
254- return lineno , lineno , ""
250+ lines = _read_source_lines (fname )
255251 if not 0 < lineno <= len (lines ):
256252 return lineno , lineno , ""
257253 start , end = _statement_bounds_in_lines (lines , lineno )
@@ -283,18 +279,8 @@ def _get_source_context(fname: str, lineno: int, context: int = 2) -> str:
283279 fname may be a bare basename (e.g. 'm_weno.fpp') or a relative path.
284280 Searches recursively under MFC_ROOT_DIR/src/ first, then the whole tree.
285281 """
286- if os .path .isabs (fname ) and os .path .isfile (fname ):
287- candidates = [fname ]
288- else :
289- candidates = glob .glob (os .path .join (MFC_ROOT_DIR , "src" , "**" , os .path .basename (fname )), recursive = True )
290- if not candidates :
291- candidates = glob .glob (os .path .join (MFC_ROOT_DIR , "**" , os .path .basename (fname )), recursive = True )
292- if not candidates :
293- return ""
294- try :
295- with open (candidates [0 ]) as fh :
296- lines = fh .readlines ()
297- except OSError :
282+ lines = _read_source_lines (fname , search_whole_tree = True )
283+ if not lines :
298284 return ""
299285 start = max (0 , lineno - context - 1 )
300286 end = min (len (lines ), lineno + context )
@@ -589,13 +575,10 @@ def _find_binary(name: str) -> str:
589575 return max (candidates , key = os .path .getmtime ) if candidates else ""
590576
591577
592- def _find_dd_sym (verrou_bin : str ) -> str :
593- c = os .path .join (os .path .dirname (verrou_bin ), "verrou_dd_sym" )
594- return c if os .path .isfile (c ) else ""
595-
596-
597- def _find_dd_line (verrou_bin : str ) -> str :
598- c = os .path .join (os .path .dirname (verrou_bin ), "verrou_dd_line" )
578+ def _find_dd_tool (verrou_bin : str , tool : str ) -> str :
579+ """Path to a verrou_dd_* tool (e.g. 'verrou_dd_sym') next to the verrou binary,
580+ or '' if absent."""
581+ c = os .path .join (os .path .dirname (verrou_bin ), tool )
599582 return c if os .path .isfile (c ) else ""
600583
601584
@@ -1152,19 +1135,26 @@ def _run_dd_tool(
11521135 return summary_lines
11531136
11541137
1138+ def _setup_dd_run (case : dict , verrou_bin : str , sim_bin : str , work_dir : str , dd_dir : str , threshold : float ):
1139+ """Write dd_run.sh and dd_cmp.py for a verrou_dd_* run into dd_dir; return their
1140+ paths. The threshold falls back to _DD_FALLBACK_THRESHOLD when unset."""
1141+ os .makedirs (dd_dir , exist_ok = True )
1142+ dd_run_sh = os .path .join (dd_dir , "dd_run.sh" )
1143+ dd_cmp_py = os .path .join (dd_dir , "dd_cmp.py" )
1144+ _write_dd_run_sh (dd_run_sh , verrou_bin , sim_bin , work_dir )
1145+ _write_dd_cmp_py (dd_cmp_py , case ["compare" ], threshold if threshold is not None else _DD_FALLBACK_THRESHOLD )
1146+ return dd_run_sh , dd_cmp_py
1147+
1148+
11551149def _run_dd_sym (case : dict , verrou_bin : str , sim_bin : str , work_dir : str , log_dir : str , threshold : float = None ) -> list :
11561150 """Run verrou_dd_sym; return list of responsible symbol names."""
1157- dd_bin = _find_dd_sym (verrou_bin )
1151+ dd_bin = _find_dd_tool (verrou_bin , "verrou_dd_sym" )
11581152 if not dd_bin :
11591153 cons .print (" [dim]verrou_dd_sym not found; skipping delta-debug[/dim]" )
11601154 return []
11611155
11621156 dd_dir = os .path .join (log_dir , case ["name" ])
1163- os .makedirs (dd_dir , exist_ok = True )
1164- dd_run_sh = os .path .join (dd_dir , "dd_run.sh" )
1165- dd_cmp_py = os .path .join (dd_dir , "dd_cmp.py" )
1166- _write_dd_run_sh (dd_run_sh , verrou_bin , sim_bin , work_dir )
1167- _write_dd_cmp_py (dd_cmp_py , case ["compare" ], threshold if threshold is not None else _DD_FALLBACK_THRESHOLD )
1157+ dd_run_sh , dd_cmp_py = _setup_dd_run (case , verrou_bin , sim_bin , work_dir , dd_dir , threshold )
11681158 _run_dd_tool (dd_bin , dd_dir , dd_run_sh , dd_cmp_py , _dd_env (verrou_bin ), "dd_sym.log" , "dd.sym" , "verrou_dd_sym" )
11691159 cons .print (f" [dim]dd_sym logs: { dd_dir } [/dim]" )
11701160 return _parse_rddmin_syms (os .path .join (dd_dir , "dd.sym" , "rddmin_summary" ))
@@ -1179,18 +1169,13 @@ def _run_dd_line(
11791169 threshold : float = None ,
11801170) -> list :
11811171 """Run verrou_dd_line; return [{path, start, end, macro}] location dicts."""
1182- dd_bin = _find_dd_line (verrou_bin )
1172+ dd_bin = _find_dd_tool (verrou_bin , "verrou_dd_line" )
11831173 if not dd_bin :
11841174 cons .print (" [dim]verrou_dd_line not found; skipping line-level debug[/dim]" )
11851175 return []
11861176
11871177 dd_dir = os .path .join (log_dir , case ["name" ])
1188- os .makedirs (dd_dir , exist_ok = True )
1189- dd_run_sh = os .path .join (dd_dir , "dd_run.sh" )
1190- dd_cmp_py = os .path .join (dd_dir , "dd_cmp.py" )
1191- effective_threshold = threshold if threshold is not None else _DD_FALLBACK_THRESHOLD
1192- _write_dd_run_sh (dd_run_sh , verrou_bin , sim_bin , work_dir )
1193- _write_dd_cmp_py (dd_cmp_py , case ["compare" ], effective_threshold )
1178+ dd_run_sh , dd_cmp_py = _setup_dd_run (case , verrou_bin , sim_bin , work_dir , dd_dir , threshold )
11941179 _run_dd_tool (dd_bin , dd_dir , dd_run_sh , dd_cmp_py , _dd_env (verrou_bin ), "dd_line.log" , "dd.line" , "verrou_dd_line" )
11951180 return _parse_rddmin_locs (os .path .join (dd_dir , "dd.line" , "rddmin_summary" ))
11961181
@@ -1217,6 +1202,26 @@ def _source_perturb_dev(verrou_bin, sim_bin, work_dir, ref_dir, conf_dir, src_li
12171202 return _max_diff_np (ref_dir , run_dir , compare )
12181203
12191204
1205+ def _capture_gen_source (verrou_bin , sim_bin , work_dir , run_dir , gen_path ):
1206+ """Run nearest-rounding with --gen-source to capture the symbol-correct
1207+ executed source lines (FILE\\ tLINE\\ tSYMBOL); return them, or None on failure."""
1208+ try :
1209+ _run_simulation_verrou (
1210+ verrou_bin ,
1211+ sim_bin ,
1212+ work_dir ,
1213+ run_dir ,
1214+ rounding_mode = "nearest" ,
1215+ extra_flags = [f"--gen-source={ gen_path } " ],
1216+ )
1217+ except MFCException :
1218+ return None
1219+ if not os .path .isfile (gen_path ):
1220+ return None
1221+ with open (gen_path ) as fh :
1222+ return fh .readlines ()
1223+
1224+
12201225def _run_confirmation (case , verrou_bin , sim_bin , work_dir , ref_dir , dd_line_locs , dd_threshold , float_proxy ):
12211226 """Positive control for dd_line: perturb ONLY the suspect lines and confirm
12221227 the instability reproduces, then rank each line by its individual share.
@@ -1237,22 +1242,9 @@ def _run_confirmation(case, verrou_bin, sim_bin, work_dir, ref_dir, dd_line_locs
12371242 return None , None , dd_line_locs
12381243 conf_dir = os .path .join (work_dir , "confirm" )
12391244 os .makedirs (conf_dir , exist_ok = True )
1240- gen_path = os .path .join (conf_dir , "gen_source.txt" )
1241- try :
1242- _run_simulation_verrou (
1243- verrou_bin ,
1244- sim_bin ,
1245- work_dir ,
1246- conf_dir ,
1247- rounding_mode = "nearest" ,
1248- extra_flags = [f"--gen-source={ gen_path } " ],
1249- )
1250- except MFCException :
1245+ gen_lines = _capture_gen_source (verrou_bin , sim_bin , work_dir , conf_dir , os .path .join (conf_dir , "gen_source.txt" ))
1246+ if gen_lines is None :
12511247 return None , None , dd_line_locs
1252- if not os .path .isfile (gen_path ):
1253- return None , None , dd_line_locs
1254- with open (gen_path ) as fh :
1255- gen_lines = fh .readlines ()
12561248 compare = case ["compare" ]
12571249
12581250 # whole-set positive control
@@ -1298,23 +1290,13 @@ def _disambiguate_instances(case, prec_sim_bin, verrou_bin, work_dir, hotspot_fi
12981290 prec_dir = os .path .join (work_dir , "precision" )
12991291 ref_dir = os .path .join (prec_dir , "ref" )
13001292 os .makedirs (ref_dir , exist_ok = True )
1301- gen_path = os .path .join (prec_dir , "gen_source.txt" )
13021293 try :
13031294 _run_simulation_verrou (verrou_bin , prec_sim_bin , work_dir , ref_dir , rounding_mode = "nearest" )
1304- _run_simulation_verrou (
1305- verrou_bin ,
1306- prec_sim_bin ,
1307- work_dir ,
1308- prec_dir ,
1309- rounding_mode = "nearest" ,
1310- extra_flags = [f"--gen-source={ gen_path } " ],
1311- )
13121295 except MFCException :
13131296 return []
1314- if not os .path .isfile (gen_path ):
1297+ gen_lines = _capture_gen_source (verrou_bin , prec_sim_bin , work_dir , prec_dir , os .path .join (prec_dir , "gen_source.txt" ))
1298+ if gen_lines is None :
13151299 return []
1316- with open (gen_path ) as fh :
1317- gen_lines = fh .readlines ()
13181300
13191301 f90_file = os .path .join (sidecar_dir , os .path .basename (hotspot_file ) + ".f90" )
13201302 compare = case ["compare" ]
@@ -1336,6 +1318,27 @@ def _disambiguate_instances(case, prec_sim_bin, verrou_bin, work_dir, hotspot_fi
13361318 return results
13371319
13381320
1321+ def _blank_result (name : str ) -> dict :
1322+ """A result dict with every field at its empty/unmeasured default."""
1323+ return {
1324+ "name" : name ,
1325+ "passed" : False ,
1326+ "max_dev" : float ("inf" ),
1327+ "sig_bits" : None ,
1328+ "float_proxy" : None ,
1329+ "vprec" : [],
1330+ "dd_sym_syms" : [],
1331+ "dd_line_locs" : [],
1332+ "dd_line_confirmed" : None ,
1333+ "dd_line_confirm_dev" : None ,
1334+ "cancellation_locs" : [],
1335+ "cancellation_bits" : {},
1336+ "mca_dev" : None ,
1337+ "mca_sigbits" : None ,
1338+ "float_max_locs" : [],
1339+ }
1340+
1341+
13391342def _run_case (
13401343 case : dict ,
13411344 verrou_bin : str ,
@@ -1362,23 +1365,7 @@ def _run_case(
13621365 cons .print (f" pass floor: >= { MIN_SIG_BITS } significant bits retained" )
13631366
13641367 work_dir = tempfile .mkdtemp (prefix = f"mfc-fps-{ name } -" )
1365- result = {
1366- "name" : name ,
1367- "passed" : False ,
1368- "max_dev" : float ("inf" ),
1369- "sig_bits" : None ,
1370- "float_proxy" : None ,
1371- "vprec" : [],
1372- "dd_sym_syms" : [],
1373- "dd_line_locs" : [],
1374- "dd_line_confirmed" : None ,
1375- "dd_line_confirm_dev" : None ,
1376- "cancellation_locs" : [],
1377- "cancellation_bits" : {},
1378- "mca_dev" : None ,
1379- "mca_sigbits" : None ,
1380- "float_max_locs" : [],
1381- }
1368+ result = _blank_result (name )
13821369 try :
13831370 cons .print (" [dim]running pre_process...[/dim]" )
13841371 _write_inp (case ["sim" ], "simulation" , work_dir )
@@ -1615,6 +1602,14 @@ def _emit_github_annotations(results: list):
16151602 print (f"::notice title=FP cancellation [{ r ['name' ]} ]::{ n_cc - 3 } more cancellation site(s) not annotated inline; see the step summary" , flush = True )
16161603
16171604
1605+ def _more_md (total : int , shown : int , noun : str ) -> str :
1606+ """Markdown bullet noting `total - shown` further items elided from a list,
1607+ or '' when nothing was truncated."""
1608+ if total <= shown :
1609+ return ""
1610+ return f"- _…and { total - shown } more { noun } ; see fp-stability-logs/_"
1611+
1612+
16181613def _emit_github_summary (results : list , n_samples : int ):
16191614 """Write a markdown results table to GITHUB_STEP_SUMMARY.
16201615
@@ -1681,8 +1676,9 @@ def _emit_github_summary(results: list, n_samples: int):
16811676 for e in ordered [:15 ]:
16821677 lost = e ["bits" ] / math .log2 (10 )
16831678 md .append (f"- **≥ { lost :.0f} digits lost** (~{ _digits_left (e ['bits' ]):.0f} of 16 left) — `{ e ['where' ]} `" + (f" — `{ e ['text' ]} `" if e ["text" ] else "" ))
1684- if len (ordered ) > 15 :
1685- md .append (f"- _…and { len (ordered ) - 15 } more statement(s); see fp-stability-logs/_" )
1679+ footer = _more_md (len (ordered ), 15 , "statement(s)" )
1680+ if footer :
1681+ md .append (footer )
16861682 md .append ("" )
16871683
16881684 # VPREC sweep — one column per bit level, ❌ where bits retained < floor
@@ -1747,8 +1743,9 @@ def _emit_github_summary(results: list, n_samples: int):
17471743 for line in snippet .splitlines ():
17481744 md .append (f" { line } " )
17491745 md .append (" ```" )
1750- if len (r ["dd_line_locs" ]) > 10 :
1751- md .append (f"- _…and { len (r ['dd_line_locs' ]) - 10 } more hotspot(s); see fp-stability-logs/_" )
1746+ footer = _more_md (len (r ["dd_line_locs" ]), 10 , "hotspot(s)" )
1747+ if footer :
1748+ md .append (footer )
17521749 md .append ("" )
17531750 md .append ("</details>\n " )
17541751
@@ -1771,8 +1768,9 @@ def _emit_github_summary(results: list, n_samples: int):
17711768 md .append (f"**`{ r ['name' ]} `** — { len (r ['float_max_locs' ])} site(s)\n " )
17721769 for fname , lineno in r ["float_max_locs" ][:10 ]:
17731770 md .append (f"- `{ fname } :{ lineno } `" )
1774- if len (r ["float_max_locs" ]) > 10 :
1775- md .append (f"- _…and { len (r ['float_max_locs' ]) - 10 } more site(s); see fp-stability-logs/_" )
1771+ footer = _more_md (len (r ["float_max_locs" ]), 10 , "site(s)" )
1772+ if footer :
1773+ md .append (footer )
17761774 md .append ("" )
17771775
17781776 with open (summary_path , "a" ) as f :
@@ -1857,23 +1855,7 @@ def fp_stability():
18571855 )
18581856 except MFCException as exc :
18591857 cons .print (f" [bold red]ERROR[/bold red]: { exc } " )
1860- r = {
1861- "name" : case ["name" ],
1862- "passed" : False ,
1863- "max_dev" : float ("inf" ),
1864- "sig_bits" : None ,
1865- "float_proxy" : None ,
1866- "vprec" : [],
1867- "dd_sym_syms" : [],
1868- "dd_line_locs" : [],
1869- "dd_line_confirmed" : None ,
1870- "dd_line_confirm_dev" : None ,
1871- "cancellation_locs" : [],
1872- "cancellation_bits" : {},
1873- "mca_dev" : None ,
1874- "mca_sigbits" : None ,
1875- "float_max_locs" : [],
1876- }
1858+ r = _blank_result (case ["name" ])
18771859 results .append (r )
18781860
18791861 elapsed = time .time () - start
0 commit comments