2727# But `kani list` runs on this fork, so it can still see it and add it to the total functions under contract.
2828# - See #TODOs for known limitations.
2929
30+ def str_to_bool (string : str ):
31+ match string .strip ().lower ():
32+ case "true" :
33+ return True
34+ case "false" :
35+ return False
36+ case _:
37+ print (f"Unexpected to-be-Boolean string { string } " )
38+ sys .exit (1 )
39+
40+
3041# Process the results from Kani's std-analysis.sh script for each crate.
31- # TODO For now, we just handle "core", but we should process all crates in the library.
3242class GenericSTDMetrics ():
33- def __init__ (self , results_dir ):
43+ def __init__ (self , results_dir , crate ):
3444 self .results_directory = results_dir
45+ self .crate = crate
3546 self .unsafe_fns_count = 0
3647 self .safe_abstractions_count = 0
3748 self .safe_fns_count = 0
3849 self .unsafe_fns = []
50+ self .unsafe_fns_with_loop = []
3951 self .safe_abstractions = []
52+ self .safe_abstractions_with_loop = []
4053 self .safe_fns = []
54+ self .safe_fns_with_loop = []
4155
4256 self .read_std_analysis ()
4357
4458 # Read {crate}_overall_counts.csv
4559 # and return the number of unsafe functions and safe abstractions
4660 def read_overall_counts (self ):
47- file_path = f"{ self .results_directory } /core_scan_overall .csv"
61+ file_path = f"{ self .results_directory } /{ self . crate } _scan_overall .csv"
4862 with open (file_path , 'r' ) as f :
4963 csv_reader = csv .reader (f , delimiter = ';' )
5064 counts = {row [0 ]: int (row [1 ]) for row in csv_reader if len (row ) >= 2 }
@@ -55,54 +69,60 @@ def read_overall_counts(self):
5569 # Read {crate}_scan_functions.csv
5670 # and return an array of the unsafe functions and the safe abstractions
5771 def read_scan_functions (self ):
58- expected_header_start = "name;is_unsafe;has_unsafe_ops"
59- file_path = f"{ self .results_directory } /core_scan_functions .csv"
72+ expected_header_start = "name;is_unsafe;has_unsafe_ops;has_unsupported_input;has_loop "
73+ file_path = f"{ self .results_directory } /{ self . crate } _scan_functions .csv"
6074
6175 with open (file_path , 'r' ) as f :
6276 csv_reader = csv .reader (f , delimiter = ';' , quotechar = '"' )
6377
6478 # The row parsing logic below assumes the column structure in expected_header_start,
6579 # so assert that is how the header begins before continuing
6680 header = next (csv_reader )
67- header_str = ';' .join (header [:3 ])
81+ header_str = ';' .join (header [:5 ])
6882 if not header_str .startswith (expected_header_start ):
6983 print (f"Error: Unexpected CSV header in { file_path } " )
7084 print (f"Expected header to start with: { expected_header_start } " )
7185 print (f"Actual header: { header_str } " )
7286 sys .exit (1 )
7387
7488 for row in csv_reader :
75- if len (row ) >= 3 :
89+ if len (row ) >= 5 :
7690 name , is_unsafe , has_unsafe_ops = row [0 ], row [1 ], row [2 ]
91+ has_unsupported_input , has_loop = row [3 ], row [4 ]
7792 # An unsafe function is a function for which is_unsafe=true
78- if is_unsafe . strip () == "true" :
93+ if str_to_bool ( is_unsafe ) :
7994 self .unsafe_fns .append (name )
95+ if str_to_bool (has_loop ):
96+ self .unsafe_fns_with_loop .append (name )
8097 else :
81- assert is_unsafe .strip () == "false" # sanity check against malformed data
8298 self .safe_fns .append (name )
99+ if str_to_bool (has_loop ):
100+ self .safe_fns_with_loop .append (name )
83101 # A safe abstraction is a safe function with unsafe ops
84- if has_unsafe_ops . strip () == "true" :
102+ if str_to_bool ( has_unsafe_ops ) :
85103 self .safe_abstractions .append (name )
104+ if str_to_bool (has_loop ):
105+ self .safe_abstractions_with_loop .append (name )
86106
87107 def read_std_analysis (self ):
88108 self .read_overall_counts ()
89109 self .read_scan_functions ()
90110
91111 # Sanity checks
92112 if len (self .unsafe_fns ) != self .unsafe_fns_count :
93- print (f"Number of unsafe functions does not match core_scan_functions .csv" )
113+ print (f"Number of unsafe functions does not match { self . crate } _scan_functions .csv" )
94114 print (f"UNSAFE_FNS_COUNT: { self .unsafe_fns_count } " )
95115 print (f"UNSAFE_FNS length: { len (self .unsafe_fns )} " )
96116 sys .exit (1 )
97117
98118 if len (self .safe_abstractions ) != self .safe_abstractions_count :
99- print (f"Number of safe abstractions does not match core_scan_functions .csv" )
119+ print (f"Number of safe abstractions does not match { self . crate } _scan_functions .csv" )
100120 print (f"SAFE_ABSTRACTIONS_COUNT: { self .safe_abstractions_count } " )
101121 print (f"SAFE_ABSTRACTIONS length: { len (self .safe_abstractions )} " )
102122 sys .exit (1 )
103123
104124 if len (self .safe_fns ) != self .safe_fns_count :
105- print (f"Number of safe functions does not match core_scan_functions .csv" )
125+ print (f"Number of safe functions does not match { self . crate } _scan_functions .csv" )
106126 print (f"SAFE_FNS_COUNT: { self .safe_fns_count } " )
107127 print (f"SAFE_FNS length: { len (self .safe_fns )} " )
108128 sys .exit (1 )
@@ -140,11 +160,33 @@ def read_kani_list_data(self, kani_list_filepath):
140160# Generate metrics about Kani's application to the standard library over time
141161# by reading past metrics from metrics_file, then computing today's metrics.
142162class KaniSTDMetricsOverTime ():
143- def __init__ (self , metrics_file ):
163+ def __init__ (self , metrics_file , crate ):
164+ self .crate = crate
144165 self .dates = []
145- self .unsafe_metrics = ['total_unsafe_fns' , 'unsafe_fns_under_contract' , 'verified_unsafe_fns_under_contract' ]
146- self .safe_abstr_metrics = ['total_safe_abstractions' , 'safe_abstractions_under_contract' , 'verified_safe_abstractions_under_contract' ]
147- self .safe_metrics = ['total_safe_fns' , 'safe_fns_under_contract' , 'verified_safe_fns_under_contract' ]
166+ self .unsafe_metrics = [
167+ 'total_unsafe_fns' ,
168+ 'total_unsafe_fns_with_loop' ,
169+ 'unsafe_fns_under_contract' ,
170+ 'unsafe_fns_with_loop_under_contract' ,
171+ 'verified_unsafe_fns_under_contract' ,
172+ 'verified_unsafe_fns_with_loop_under_contract'
173+ ]
174+ self .safe_abstr_metrics = [
175+ 'total_safe_abstractions' ,
176+ 'total_safe_abstractions_with_loop' ,
177+ 'safe_abstractions_under_contract' ,
178+ 'safe_abstractions_with_loop_under_contract' ,
179+ 'verified_safe_abstractions_under_contract' ,
180+ 'verified_safe_abstractions_with_loop_under_contract'
181+ ]
182+ self .safe_metrics = [
183+ 'total_safe_fns' ,
184+ 'total_safe_fns_with_loop' ,
185+ 'safe_fns_under_contract' ,
186+ 'safe_fns_with_loop_under_contract' ,
187+ 'verified_safe_fns_under_contract' ,
188+ 'verified_safe_fns_with_loop_under_contract'
189+ ]
148190 # The keys in these dictionaries are unsafe_metrics, safe_abstr_metrics, and safe_metrics, respectively; see update_plot_metrics()
149191 self .unsafe_plot_data = defaultdict (list )
150192 self .safe_abstr_plot_data = defaultdict (list )
@@ -157,12 +199,9 @@ def __init__(self, metrics_file):
157199
158200 # Read historical data from self.metrics_file and initialize the date range.
159201 def read_historical_data (self ):
160- try :
161- with open (self .metrics_file , 'r' ) as f :
162- all_data = json .load (f )["results" ]
163- self .update_plot_metrics (all_data )
164- except FileNotFoundError :
165- all_data = {}
202+ with open (self .metrics_file , 'r' ) as f :
203+ all_data = json .load (f )["results" ]
204+ self .update_plot_metrics (all_data )
166205
167206 self .dates = [datetime .strptime (data ["date" ], '%Y-%m-%d' ).date () for data in all_data ]
168207
@@ -173,15 +212,15 @@ def update_plot_metrics(self, all_data):
173212 for data in all_data :
174213 for metric in self .unsafe_metrics :
175214 if not metric .startswith ("verified" ):
176- self .unsafe_plot_data [metric ].append (data [ metric ] )
215+ self .unsafe_plot_data [metric ].append (data . get ( metric , 0 ) )
177216
178217 for metric in self .safe_abstr_metrics :
179218 if not metric .startswith ("verified" ):
180- self .safe_abstr_plot_data [metric ].append (data [ metric ] )
219+ self .safe_abstr_plot_data [metric ].append (data . get ( metric , 0 ) )
181220
182221 for metric in self .safe_metrics :
183222 if not metric .startswith ("verified" ):
184- self .safe_plot_data [metric ].append (data [ metric ] )
223+ self .safe_plot_data [metric ].append (data . get ( metric , 0 ) )
185224
186225 # Read output from kani list and std-analysis.sh, then compare their outputs to compute Kani-specific metrics
187226 # and write the results to {self.metrics_file}
@@ -190,41 +229,68 @@ def compute_metrics(self, kani_list_filepath, analysis_results_dir):
190229
191230 # Process the `kani list` and `std-analysis.sh` data
192231 kani_data = KaniListSTDMetrics (kani_list_filepath )
193- generic_metrics = GenericSTDMetrics (analysis_results_dir )
232+ generic_metrics = GenericSTDMetrics (analysis_results_dir , self . crate )
194233
195234 print ("Comparing kani-list output to std-analysis.sh output and computing metrics..." )
196235
197236 (unsafe_fns_under_contract , verified_unsafe_fns_under_contract ) = (0 , 0 )
237+ unsafe_fns_with_loop_under_contract = 0
238+ verified_unsafe_fns_with_loop_under_contract = 0
198239 (safe_abstractions_under_contract , verified_safe_abstractions_under_contract ) = (0 , 0 )
240+ safe_abstractions_with_loop_under_contract = 0
241+ verified_safe_abstractions_with_loop_under_contract = 0
199242 (safe_fns_under_contract , verified_safe_fns_under_contract ) = (0 , 0 )
243+ safe_fns_with_loop_under_contract = 0
244+ verified_safe_fns_with_loop_under_contract = 0
200245
201246 for (func_under_contract , has_harnesses ) in kani_data .fns_under_contract :
202247 if func_under_contract in generic_metrics .unsafe_fns :
248+ has_loop = int (func_under_contract in
249+ generic_metrics .unsafe_fns_with_loop )
203250 unsafe_fns_under_contract += 1
251+ unsafe_fns_with_loop_under_contract += has_loop
204252 if has_harnesses :
205253 verified_unsafe_fns_under_contract += 1
254+ verified_unsafe_fns_with_loop_under_contract += has_loop
206255 if func_under_contract in generic_metrics .safe_abstractions :
256+ has_loop = int (func_under_contract in
257+ generic_metrics .safe_abstractions_with_loop )
207258 safe_abstractions_under_contract += 1
259+ safe_abstractions_with_loop_under_contract += has_loop
208260 if has_harnesses :
209261 verified_safe_abstractions_under_contract += 1
262+ verified_safe_abstractions_with_loop_under_contract += has_loop
210263 if func_under_contract in generic_metrics .safe_fns :
264+ has_loop = int (func_under_contract in
265+ generic_metrics .safe_fns_with_loop )
211266 safe_fns_under_contract += 1
267+ safe_fns_with_loop_under_contract += has_loop
212268 if has_harnesses :
213269 verified_safe_fns_under_contract += 1
270+ verified_safe_fns_with_loop_under_contract += has_loop
214271
215272 # Keep the keys here in sync with unsafe_metrics, safe_metrics, and safe_abstr_metrics
216273 data = {
217274 "date" : self .date ,
218275 "total_unsafe_fns" : generic_metrics .unsafe_fns_count ,
276+ "total_unsafe_fns_with_loop" : len (generic_metrics .unsafe_fns_with_loop ),
219277 "total_safe_abstractions" : generic_metrics .safe_abstractions_count ,
278+ "total_safe_abstractions_with_loop" : len (generic_metrics .safe_abstractions_with_loop ),
220279 "total_safe_fns" : generic_metrics .safe_fns_count ,
280+ "total_safe_fns_with_loop" : len (generic_metrics .safe_fns_with_loop ),
221281 "unsafe_fns_under_contract" : unsafe_fns_under_contract ,
282+ "unsafe_fns_with_loop_under_contract" : unsafe_fns_with_loop_under_contract ,
222283 "verified_unsafe_fns_under_contract" : verified_unsafe_fns_under_contract ,
284+ "verified_unsafe_fns_with_loop_under_contract" : verified_unsafe_fns_with_loop_under_contract ,
223285 "safe_abstractions_under_contract" : safe_abstractions_under_contract ,
286+ "safe_abstractions_with_loop_under_contract" : safe_abstractions_with_loop_under_contract ,
224287 "verified_safe_abstractions_under_contract" : verified_safe_abstractions_under_contract ,
288+ "verified_safe_abstractions_with_loop_under_contract" : verified_safe_abstractions_with_loop_under_contract ,
225289 "safe_fns_under_contract" : safe_fns_under_contract ,
290+ "safe_fns_with_loop_under_contract" : safe_fns_with_loop_under_contract ,
226291 "verified_safe_fns_under_contract" : verified_safe_fns_under_contract ,
227- "total_functions_under_contract" : kani_data .total_fns_under_contract ,
292+ "verified_safe_fns_with_loop_under_contract" : verified_safe_fns_with_loop_under_contract ,
293+ "total_functions_under_contract_all_crates" : kani_data .total_fns_under_contract ,
228294 }
229295
230296 self .update_plot_metrics ([data ])
@@ -243,7 +309,27 @@ def compute_metrics(self, kani_list_filepath, analysis_results_dir):
243309 def plot_single (self , data , title , filename , plot_dir ):
244310 plt .figure (figsize = (14 , 8 ))
245311
246- colors = ['#1f77b4' , '#ff7f0e' , '#2ca02c' , '#d62728' , '#946F7bd' , '#8c564b' , '#e377c2' , '#7f7f7f' , '#bcbd22' , '#17becf' ]
312+ colors = [
313+ '#1f77b4' , #total_unsafe_fns
314+ '#941fb4' , #total_unsafe_fns_with_loop
315+ '#ff7f0e' , #total_safe_abstractions
316+ '#abff0e' , #total_safe_abstractions_with_loop
317+ '#2ca02c' , #total_safe_fns
318+ '#a02c8d' , #total_safe_fns_with_loop
319+ '#d62728' , #unsafe_fns_under_contract
320+ '#27d6aa' , #unsafe_fns_with_loop_under_contract
321+ '#9467bd' , #verified_unsafe_fns_under_contract
322+ '#67acbd' , #verified_unsafe_fns_with_loop_under_contract
323+ '#8c564b' , #safe_abstractions_under_contract
324+ '#8c814b' , #safe_abstractions_with_loop_under_contract
325+ '#e377c2' , #verified_safe_abstractions_under_contract
326+ '#a277e3' , #verified_safe_abstractions_with_loop_under_contract
327+ '#7f7f7f' , #safe_fns_under_contract
328+ '#9e6767' , #safe_fns_with_loop_under_contract
329+ '#bcbd22' , #verified_safe_fns_under_contract
330+ '#49bd22' , #verified_safe_fns_with_loop_under_contract
331+ '#17becf' #total_functions_under_contract
332+ ]
247333
248334 for i , (metric , values ) in enumerate (data .items ()):
249335 color = colors [i % len (colors )]
@@ -280,16 +366,32 @@ def plot_single(self, data, title, filename, plot_dir):
280366 print (f"PNG graph generated: { outfile } " )
281367
282368 def plot (self , plot_dir ):
283- self .plot_single (self .unsafe_plot_data , title = "Contracts on Unsafe Functions in core" , filename = "core_unsafe_metrics.png" , plot_dir = plot_dir )
284- self .plot_single (self .safe_abstr_plot_data , title = "Contracts on Safe Abstractions in core" , filename = "core_safe_abstractions_metrics.png" , plot_dir = plot_dir )
285- self .plot_single (self .safe_plot_data , title = "Contracts on Safe Functions in core" , filename = "core_safe_metrics.png" , plot_dir = plot_dir )
369+ self .plot_single (
370+ self .unsafe_plot_data ,
371+ title = f"Contracts on Unsafe Functions in { self .crate } " ,
372+ filename = f"{ self .crate } _unsafe_metrics.png" ,
373+ plot_dir = plot_dir )
374+ self .plot_single (
375+ self .safe_abstr_plot_data ,
376+ title = f"Contracts on Safe Abstractions in { self .crate } " ,
377+ filename = f"{ self .crate } _safe_abstractions_metrics.png" ,
378+ plot_dir = plot_dir )
379+ self .plot_single (
380+ self .safe_plot_data ,
381+ title = f"Contracts on Safe Functions in { self .crate } " ,
382+ filename = f"{ self .crate } _safe_metrics.png" ,
383+ plot_dir = plot_dir )
286384
287385def main ():
288386 parser = argparse .ArgumentParser (description = "Generate metrics about Kani's application to the standard library." )
387+ parser .add_argument ('--crate' ,
388+ type = str ,
389+ required = True ,
390+ help = "Name of standard library crate to produce metrics for" )
289391 parser .add_argument ('--metrics-file' ,
290392 type = str ,
291- default = "metrics-data.json" ,
292- help = "Path to the JSON file containing metrics data (default: metrics-data.json) " )
393+ required = True ,
394+ help = "Path to the JSON file containing metrics data" )
293395 parser .add_argument ('--kani-list-file' ,
294396 type = str ,
295397 default = "kani-list.json" ,
@@ -308,7 +410,7 @@ def main():
308410
309411 args = parser .parse_args ()
310412
311- metrics = KaniSTDMetricsOverTime (args .metrics_file )
413+ metrics = KaniSTDMetricsOverTime (args .metrics_file , args . crate )
312414
313415 if args .plot_only :
314416 metrics .plot (args .plot_dir )
0 commit comments