27
27
# But `kani list` runs on this fork, so it can still see it and add it to the total functions under contract.
28
28
# - See #TODOs for known limitations.
29
29
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
+
30
41
# 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.
32
42
class GenericSTDMetrics ():
33
- def __init__ (self , results_dir ):
43
+ def __init__ (self , results_dir , crate ):
34
44
self .results_directory = results_dir
45
+ self .crate = crate
35
46
self .unsafe_fns_count = 0
36
47
self .safe_abstractions_count = 0
37
48
self .safe_fns_count = 0
38
49
self .unsafe_fns = []
50
+ self .unsafe_fns_with_loop = []
39
51
self .safe_abstractions = []
52
+ self .safe_abstractions_with_loop = []
40
53
self .safe_fns = []
54
+ self .safe_fns_with_loop = []
41
55
42
56
self .read_std_analysis ()
43
57
44
58
# Read {crate}_overall_counts.csv
45
59
# and return the number of unsafe functions and safe abstractions
46
60
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"
48
62
with open (file_path , 'r' ) as f :
49
63
csv_reader = csv .reader (f , delimiter = ';' )
50
64
counts = {row [0 ]: int (row [1 ]) for row in csv_reader if len (row ) >= 2 }
@@ -55,54 +69,60 @@ def read_overall_counts(self):
55
69
# Read {crate}_scan_functions.csv
56
70
# and return an array of the unsafe functions and the safe abstractions
57
71
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"
60
74
61
75
with open (file_path , 'r' ) as f :
62
76
csv_reader = csv .reader (f , delimiter = ';' , quotechar = '"' )
63
77
64
78
# The row parsing logic below assumes the column structure in expected_header_start,
65
79
# so assert that is how the header begins before continuing
66
80
header = next (csv_reader )
67
- header_str = ';' .join (header [:3 ])
81
+ header_str = ';' .join (header [:5 ])
68
82
if not header_str .startswith (expected_header_start ):
69
83
print (f"Error: Unexpected CSV header in { file_path } " )
70
84
print (f"Expected header to start with: { expected_header_start } " )
71
85
print (f"Actual header: { header_str } " )
72
86
sys .exit (1 )
73
87
74
88
for row in csv_reader :
75
- if len (row ) >= 3 :
89
+ if len (row ) >= 5 :
76
90
name , is_unsafe , has_unsafe_ops = row [0 ], row [1 ], row [2 ]
91
+ has_unsupported_input , has_loop = row [3 ], row [4 ]
77
92
# An unsafe function is a function for which is_unsafe=true
78
- if is_unsafe . strip () == "true" :
93
+ if str_to_bool ( is_unsafe ) :
79
94
self .unsafe_fns .append (name )
95
+ if str_to_bool (has_loop ):
96
+ self .unsafe_fns_with_loop .append (name )
80
97
else :
81
- assert is_unsafe .strip () == "false" # sanity check against malformed data
82
98
self .safe_fns .append (name )
99
+ if str_to_bool (has_loop ):
100
+ self .safe_fns_with_loop .append (name )
83
101
# 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 ) :
85
103
self .safe_abstractions .append (name )
104
+ if str_to_bool (has_loop ):
105
+ self .safe_abstractions_with_loop .append (name )
86
106
87
107
def read_std_analysis (self ):
88
108
self .read_overall_counts ()
89
109
self .read_scan_functions ()
90
110
91
111
# Sanity checks
92
112
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" )
94
114
print (f"UNSAFE_FNS_COUNT: { self .unsafe_fns_count } " )
95
115
print (f"UNSAFE_FNS length: { len (self .unsafe_fns )} " )
96
116
sys .exit (1 )
97
117
98
118
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" )
100
120
print (f"SAFE_ABSTRACTIONS_COUNT: { self .safe_abstractions_count } " )
101
121
print (f"SAFE_ABSTRACTIONS length: { len (self .safe_abstractions )} " )
102
122
sys .exit (1 )
103
123
104
124
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" )
106
126
print (f"SAFE_FNS_COUNT: { self .safe_fns_count } " )
107
127
print (f"SAFE_FNS length: { len (self .safe_fns )} " )
108
128
sys .exit (1 )
@@ -140,11 +160,33 @@ def read_kani_list_data(self, kani_list_filepath):
140
160
# Generate metrics about Kani's application to the standard library over time
141
161
# by reading past metrics from metrics_file, then computing today's metrics.
142
162
class KaniSTDMetricsOverTime ():
143
- def __init__ (self , metrics_file ):
163
+ def __init__ (self , metrics_file , crate ):
164
+ self .crate = crate
144
165
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
+ ]
148
190
# The keys in these dictionaries are unsafe_metrics, safe_abstr_metrics, and safe_metrics, respectively; see update_plot_metrics()
149
191
self .unsafe_plot_data = defaultdict (list )
150
192
self .safe_abstr_plot_data = defaultdict (list )
@@ -157,12 +199,9 @@ def __init__(self, metrics_file):
157
199
158
200
# Read historical data from self.metrics_file and initialize the date range.
159
201
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 )
166
205
167
206
self .dates = [datetime .strptime (data ["date" ], '%Y-%m-%d' ).date () for data in all_data ]
168
207
@@ -173,15 +212,15 @@ def update_plot_metrics(self, all_data):
173
212
for data in all_data :
174
213
for metric in self .unsafe_metrics :
175
214
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 ) )
177
216
178
217
for metric in self .safe_abstr_metrics :
179
218
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 ) )
181
220
182
221
for metric in self .safe_metrics :
183
222
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 ) )
185
224
186
225
# Read output from kani list and std-analysis.sh, then compare their outputs to compute Kani-specific metrics
187
226
# and write the results to {self.metrics_file}
@@ -190,41 +229,68 @@ def compute_metrics(self, kani_list_filepath, analysis_results_dir):
190
229
191
230
# Process the `kani list` and `std-analysis.sh` data
192
231
kani_data = KaniListSTDMetrics (kani_list_filepath )
193
- generic_metrics = GenericSTDMetrics (analysis_results_dir )
232
+ generic_metrics = GenericSTDMetrics (analysis_results_dir , self . crate )
194
233
195
234
print ("Comparing kani-list output to std-analysis.sh output and computing metrics..." )
196
235
197
236
(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
198
239
(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
199
242
(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
200
245
201
246
for (func_under_contract , has_harnesses ) in kani_data .fns_under_contract :
202
247
if func_under_contract in generic_metrics .unsafe_fns :
248
+ has_loop = int (func_under_contract in
249
+ generic_metrics .unsafe_fns_with_loop )
203
250
unsafe_fns_under_contract += 1
251
+ unsafe_fns_with_loop_under_contract += has_loop
204
252
if has_harnesses :
205
253
verified_unsafe_fns_under_contract += 1
254
+ verified_unsafe_fns_with_loop_under_contract += has_loop
206
255
if func_under_contract in generic_metrics .safe_abstractions :
256
+ has_loop = int (func_under_contract in
257
+ generic_metrics .safe_abstractions_with_loop )
207
258
safe_abstractions_under_contract += 1
259
+ safe_abstractions_with_loop_under_contract += has_loop
208
260
if has_harnesses :
209
261
verified_safe_abstractions_under_contract += 1
262
+ verified_safe_abstractions_with_loop_under_contract += has_loop
210
263
if func_under_contract in generic_metrics .safe_fns :
264
+ has_loop = int (func_under_contract in
265
+ generic_metrics .safe_fns_with_loop )
211
266
safe_fns_under_contract += 1
267
+ safe_fns_with_loop_under_contract += has_loop
212
268
if has_harnesses :
213
269
verified_safe_fns_under_contract += 1
270
+ verified_safe_fns_with_loop_under_contract += has_loop
214
271
215
272
# Keep the keys here in sync with unsafe_metrics, safe_metrics, and safe_abstr_metrics
216
273
data = {
217
274
"date" : self .date ,
218
275
"total_unsafe_fns" : generic_metrics .unsafe_fns_count ,
276
+ "total_unsafe_fns_with_loop" : len (generic_metrics .unsafe_fns_with_loop ),
219
277
"total_safe_abstractions" : generic_metrics .safe_abstractions_count ,
278
+ "total_safe_abstractions_with_loop" : len (generic_metrics .safe_abstractions_with_loop ),
220
279
"total_safe_fns" : generic_metrics .safe_fns_count ,
280
+ "total_safe_fns_with_loop" : len (generic_metrics .safe_fns_with_loop ),
221
281
"unsafe_fns_under_contract" : unsafe_fns_under_contract ,
282
+ "unsafe_fns_with_loop_under_contract" : unsafe_fns_with_loop_under_contract ,
222
283
"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 ,
223
285
"safe_abstractions_under_contract" : safe_abstractions_under_contract ,
286
+ "safe_abstractions_with_loop_under_contract" : safe_abstractions_with_loop_under_contract ,
224
287
"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 ,
225
289
"safe_fns_under_contract" : safe_fns_under_contract ,
290
+ "safe_fns_with_loop_under_contract" : safe_fns_with_loop_under_contract ,
226
291
"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 ,
228
294
}
229
295
230
296
self .update_plot_metrics ([data ])
@@ -243,7 +309,27 @@ def compute_metrics(self, kani_list_filepath, analysis_results_dir):
243
309
def plot_single (self , data , title , filename , plot_dir ):
244
310
plt .figure (figsize = (14 , 8 ))
245
311
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
+ ]
247
333
248
334
for i , (metric , values ) in enumerate (data .items ()):
249
335
color = colors [i % len (colors )]
@@ -280,16 +366,32 @@ def plot_single(self, data, title, filename, plot_dir):
280
366
print (f"PNG graph generated: { outfile } " )
281
367
282
368
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 )
286
384
287
385
def main ():
288
386
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" )
289
391
parser .add_argument ('--metrics-file' ,
290
392
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" )
293
395
parser .add_argument ('--kani-list-file' ,
294
396
type = str ,
295
397
default = "kani-list.json" ,
@@ -308,7 +410,7 @@ def main():
308
410
309
411
args = parser .parse_args ()
310
412
311
- metrics = KaniSTDMetricsOverTime (args .metrics_file )
413
+ metrics = KaniSTDMetricsOverTime (args .metrics_file , args . crate )
312
414
313
415
if args .plot_only :
314
416
metrics .plot (args .plot_dir )
0 commit comments