summaryrefslogtreecommitdiff
path: root/deps/theft/theft.c
blob: 7c6244f072f13b94e5108ecad4323c94c7254dd9 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
#include <string.h>

#include "theft.h"
#include "theft_types_internal.h"
#include "theft_bloom.h"
#include "theft_mt.h"

/* Initialize a theft test runner.
 * BLOOM_BITS sets the size of the table used for detecting
 * combinations of arguments that have already been tested.
 * If 0, a default size will be chosen based on trial count.
 * (This will only be used if all property types have hash
 * callbacks defined.) The bloom filter can also be disabled
 * by setting BLOOM_BITS to THEFT_BLOOM_DISABLE.
 * 
 * Returns a NULL if malloc fails or BLOOM_BITS is out of bounds. */
struct theft *theft_init(uint8_t bloom_bits) {
    if ((bloom_bits != 0 && (bloom_bits < THEFT_BLOOM_BITS_MIN))
        || ((bloom_bits > THEFT_BLOOM_BITS_MAX) &&
            bloom_bits != THEFT_BLOOM_DISABLE)) {
        return NULL;
    }

    theft *t = malloc(sizeof(*t));
    if (t == NULL) { return NULL; }
    memset(t, 0, sizeof(*t));

    t->mt = theft_mt_init(DEFAULT_THEFT_SEED);
    if (t->mt == NULL) {
        free(t);
        return NULL;
    } else {
        t->out = stdout;
        t->requested_bloom_bits = bloom_bits;
        return t;
    }
}

/* (Re-)initialize the random number generator with a specific seed. */
void theft_set_seed(struct theft *t, uint64_t seed) {
    t->seed = seed;
    theft_mt_reset(t->mt, seed);
}

/* Get a random 64-bit integer from the test runner's PRNG. */
theft_seed theft_random(struct theft *t) {
    theft_seed ns = (theft_seed)theft_mt_random(t->mt);
    return ns;
}

/* Get a random double from the test runner's PRNG. */
double theft_random_double(struct theft *t) {
    return theft_mt_random_double(t->mt);
}

/* Change T's output stream handle to OUT. (Default: stdout.) */
void theft_set_output_stream(struct theft *t, FILE *out) { t->out = out; }

/* Check if all argument info structs have all required callbacks. */
static bool
check_all_args(struct theft_propfun_info *info, bool *all_hashable) {
    bool ah = true;
    for (int i = 0; i < info->arity; i++) {
        struct theft_type_info *ti = info->type_info[i];
        if (ti->alloc == NULL) { return false; }
        if (ti->hash == NULL) { ah = false; }
    }
    *all_hashable = ah;
    return true;
}

static theft_progress_callback_res
default_progress_cb(struct theft_trial_info *info, void *env) {
    (void)info;
    (void)env;
    return THEFT_PROGRESS_CONTINUE;
}

static void infer_arity(struct theft_propfun_info *info) {
    for (int i = 0; i < THEFT_MAX_ARITY; i++) {
        if (info->type_info[i] == NULL) {
            info->arity = i;
            break;
        }
    }
}

/* Run a series of randomized trials of a property function.
 *
 * Configuration is specified in CFG; many fields are optional.
 * See the type definition in `theft_types.h`. */
theft_run_res
theft_run(struct theft *t, struct theft_cfg *cfg) {
    if (t == NULL || cfg == NULL) {
        return THEFT_RUN_ERROR_BAD_ARGS;
    }

    struct theft_propfun_info info;
    memset(&info, 0, sizeof(info));
    info.name = cfg->name;
    info.fun = cfg->fun;
    memcpy(info.type_info, cfg->type_info, sizeof(info.type_info));
    info.always_seed_count = cfg->always_seed_count;
    info.always_seeds = cfg->always_seeds;

    if (cfg->seed) {
        theft_set_seed(t, cfg->seed);
    } else {
        theft_set_seed(t, DEFAULT_THEFT_SEED);
    }

    if (cfg->trials == 0) { cfg->trials = THEFT_DEF_TRIALS; }

    return theft_run_internal(t, &info, cfg->trials, cfg->progress_cb,
        cfg->env, cfg->report);
}

/* Actually run the trials, with all arguments made explicit. */
static theft_run_res
theft_run_internal(struct theft *t, struct theft_propfun_info *info,
        int trials, theft_progress_cb *cb, void *env,
        struct theft_trial_report *r) {

    struct theft_trial_report fake_report;
    if (r == NULL) { r = &fake_report; }
    memset(r, 0, sizeof(*r));
    
    infer_arity(info);
    if (info->arity == 0) {
        return THEFT_RUN_ERROR_BAD_ARGS;
    }

    if (t == NULL || info == NULL || info->fun == NULL
        || info->arity == 0) {
        return THEFT_RUN_ERROR_BAD_ARGS;
    }
    
    bool all_hashable = false;
    if (!check_all_args(info, &all_hashable)) {
        return THEFT_RUN_ERROR_MISSING_CALLBACK;
    }

    if (cb == NULL) { cb = default_progress_cb; }

    /* If all arguments are hashable, then attempt to use
     * a bloom filter to avoid redundant checking. */
    if (all_hashable) {
        if (t->requested_bloom_bits == 0) {
            t->requested_bloom_bits = theft_bloom_recommendation(trials);
        }
        if (t->requested_bloom_bits != THEFT_BLOOM_DISABLE) {
            t->bloom = theft_bloom_init(t->requested_bloom_bits);
        }
    }
    
    theft_seed seed = t->seed;
    theft_seed initial_seed = t->seed;
    int always_seeds = info->always_seed_count;
    if (info->always_seeds == NULL) { always_seeds = 0; }

    void *args[THEFT_MAX_ARITY];
    
    theft_progress_callback_res cres = THEFT_PROGRESS_CONTINUE;

    for (int trial = 0; trial < trials; trial++) {
        memset(args, 0xFF, sizeof(args));
        if (cres == THEFT_PROGRESS_HALT) { break; }

        /* If any seeds to always run were specified, use those before
         * reverting to the specified starting seed. */
        if (trial < always_seeds) {
            seed = info->always_seeds[trial];
        } else if ((always_seeds > 0) && (trial == always_seeds)) {
            seed = initial_seed;
        }

        struct theft_trial_info ti = {
            .name = info->name,
            .trial = trial,
            .seed = seed,
            .arity = info->arity,
            .args = args
        };

        theft_set_seed(t, seed);
        all_gen_res_t gres = gen_all_args(t, info, seed, args, env);
        switch (gres) {
        case ALL_GEN_SKIP:
            /* skip generating these args */
            ti.status = THEFT_TRIAL_SKIP;
            r->skip++;
            cres = cb(&ti, env);
            break;
        case ALL_GEN_DUP:
            /* skip these args -- probably already tried */
            ti.status = THEFT_TRIAL_DUP;
            r->dup++;
            cres = cb(&ti, env);
            break;
        default:
        case ALL_GEN_ERROR:
            /* Error while generating args */
            ti.status = THEFT_TRIAL_ERROR;
            cres = cb(&ti, env);
            return THEFT_RUN_ERROR;
        case ALL_GEN_OK:
            /* (Extracted function to avoid deep nesting here.) */
            if (!run_trial(t, info, args, cb, env, r, &ti, &cres)) {
                return THEFT_RUN_ERROR;
            }
        }

        free_args(info, args, env);

        /* Restore last known seed and generate next. */
        theft_set_seed(t, seed);
        seed = theft_random(t);
    }

    if (r->fail > 0) {
        return THEFT_RUN_FAIL;
    } else {
        return THEFT_RUN_PASS;
    }
}

/* Now that arguments have been generated, run the trial and update
 * counters, call cb with results, etc. */
static bool run_trial(struct theft *t, struct theft_propfun_info *info,
        void **args, theft_progress_cb *cb, void *env,
        struct theft_trial_report *r, struct theft_trial_info *ti,
        theft_progress_callback_res *cres) {
    if (t->bloom) { mark_called(t, info, args, env); }
    theft_trial_res tres = call_fun(info, args);
    ti->status = tres;
    switch (tres) {
    case THEFT_TRIAL_PASS:
        r->pass++;
        *cres = cb(ti, env);
        break;
    case THEFT_TRIAL_FAIL:
        if (!attempt_to_shrink(t, info, args, env)) {
            ti->status = THEFT_TRIAL_ERROR;
            *cres = cb(ti, env);
            return false;
        }
        r->fail++;
        *cres = report_on_failure(t, info, ti, cb, env);
        break;
    case THEFT_TRIAL_SKIP:
        *cres = cb(ti, env);
        r->skip++;
        break;
    case THEFT_TRIAL_DUP:
        /* user callback should not return this; fall through */
    case THEFT_TRIAL_ERROR:
        *cres = cb(ti, env);
        free_args(info, args, env);
        return false;
    }
    return true;
}

static void free_args(struct theft_propfun_info *info,
        void **args, void *env) {
    for (int i = 0; i < info->arity; i++) {
        theft_free_cb *fcb = info->type_info[i]->free;
        if (fcb && args[i] != THEFT_SKIP) {
            fcb(args[i], env);
        }
    }
}

void theft_free(struct theft *t) {
    if (t->bloom) {
        theft_bloom_dump(t->bloom);
        theft_bloom_free(t->bloom);
        t->bloom = NULL;
    }
    theft_mt_free(t->mt);
    free(t);
}

/* Actually call the property function. Its number of arguments is not
 * constrained by the typedef, but will be defined at the call site
 * here. (If info->arity is wrong, it will probably crash.) */
static theft_trial_res
call_fun(struct theft_propfun_info *info, void **args) {
    theft_trial_res res = THEFT_TRIAL_ERROR;
    switch (info->arity) {
    case 1:
        res = info->fun(args[0]);
        break;
    case 2:
        res = info->fun(args[0], args[1]);
        break;
    case 3:
        res = info->fun(args[0], args[1], args[2]);
        break;
    case 4:
        res = info->fun(args[0], args[1], args[2], args[3]);
        break;
    case 5:
        res = info->fun(args[0], args[1], args[2], args[3], args[4]);
        break;
    case 6:
        res = info->fun(args[0], args[1], args[2], args[3], args[4],
            args[5]);
        break;
    case 7:
        res = info->fun(args[0], args[1], args[2], args[3], args[4],
            args[5], args[6]);
        break;
    case 8:
        res = info->fun(args[0], args[1], args[2], args[3], args[4],
            args[5], args[6], args[7]);
        break;
    case 9:
        res = info->fun(args[0], args[1], args[2], args[3], args[4],
            args[5], args[6], args[7], args[8]);
        break;
    case 10:
        res = info->fun(args[0], args[1], args[2], args[3], args[4],
            args[5], args[6], args[7], args[8], args[9]);
        break;
    /* ... */
    default:
        return THEFT_TRIAL_ERROR;
    }
    return res;
}

/* Attempt to instantiate arguments, starting with the current seed. */
static all_gen_res_t
gen_all_args(theft *t, struct theft_propfun_info *info,
        theft_seed seed, void *args[THEFT_MAX_ARITY], void *env) {
    for (int i = 0; i < info->arity; i++) {
        struct theft_type_info *ti = info->type_info[i];
        void *p = ti->alloc(t, seed, env);
        if (p == THEFT_SKIP || p == THEFT_ERROR) {
            for (int j = 0; j < i; j++) {
                ti->free(args[j], env);
            }
            if (p == THEFT_SKIP) {
                return ALL_GEN_SKIP;
            } else {
                return ALL_GEN_ERROR;
            }
        } else {
            args[i] = p;
        }
        seed = theft_random(t);
    }

    /* check bloom filter */
    if (t->bloom && check_called(t, info, args, env)) {
        return ALL_GEN_DUP;
    }
    
    return ALL_GEN_OK;
}

/* Attempt to simplify all arguments, breadth first. Continue as long as
 * progress is made, i.e., until a local minima is reached. */
static bool
attempt_to_shrink(theft *t, struct theft_propfun_info *info,
        void *args[], void *env) {
    bool progress = false;
    do {
        progress = false;
        for (int ai = 0; ai < info->arity; ai++) {
            struct theft_type_info *ti = info->type_info[ai];
            if (ti->shrink) {
                /* attempt to simplify this argument by one step */
                shrink_res rres = attempt_to_shrink_arg(t, info, args, env, ai);
                switch (rres) {
                case SHRINK_OK:
                    progress = true;
                    break;
                case SHRINK_DEAD_END:
                    break;
                default:
                case SHRINK_ERROR:
                    return false;
                }
            }
        }
    } while (progress);

    return true;
}

/* Simplify an argument by trying all of its simplification tactics, in
 * order, and checking whether the property still fails. If it passes,
 * then revert the simplification and try another tactic.
 *
 * If the bloom filter is being used (i.e., if all arguments have hash
 * callbacks defined), then use it to skip over areas of the state
 * space that have probably already been tried. */
static shrink_res
attempt_to_shrink_arg(theft *t, struct theft_propfun_info *info,
        void *args[], void *env, int ai) {
    struct theft_type_info *ti = info->type_info[ai];

    for (uint32_t tactic = 0; tactic < THEFT_MAX_TACTICS; tactic++) {
        void *cur = args[ai];
        void *nv = ti->shrink(cur, tactic, env);
        if (nv == THEFT_NO_MORE_TACTICS) {
            return SHRINK_DEAD_END;
        } else if (nv == THEFT_ERROR) {
            return SHRINK_ERROR;
        } else if (nv == THEFT_DEAD_END) {
            continue;   /* try next tactic */
        }
        
        args[ai] = nv;
        if (t->bloom) {
            if (check_called(t, info, args, env)) {
                /* probably redundant */
                if (ti->free) { ti->free(nv, env); }
                args[ai] = cur;
                continue;
            } else {
                mark_called(t, info, args, env);
            }
        }
        theft_trial_res res = call_fun(info, args);
        
        switch (res) {
        case THEFT_TRIAL_PASS:
        case THEFT_TRIAL_SKIP:
            /* revert */
            args[ai] = cur;
            if (ti->free) { ti->free(nv, env); }
            break;
        case THEFT_TRIAL_FAIL:
            if (ti->free) { ti->free(cur, env); }
            return SHRINK_OK;
        case THEFT_TRIAL_DUP:  /* user callback should not return this */
        case THEFT_TRIAL_ERROR:
            return SHRINK_ERROR;
        }
    }
    (void)t;
    return SHRINK_DEAD_END;
}

/* Populate a buffer with hashes of all the arguments. */
static void get_arg_hash_buffer(theft_hash *buffer,
        struct theft_propfun_info *info, void **args, void *env) {
    for (int i = 0; i < info->arity; i++) {
        buffer[i] = info->type_info[i]->hash(args[i], env);
    }    
}

/* Mark the tuple of argument instances as called in the bloom filter. */
static void mark_called(theft *t, struct theft_propfun_info *info,
        void **args, void *env) {
    theft_hash buffer[THEFT_MAX_ARITY];
    get_arg_hash_buffer(buffer, info, args, env);
    theft_bloom_mark(t->bloom, (uint8_t *)buffer,
        info->arity * sizeof(theft_hash));
}

/* Check if this combination of argument instances has been called. */
static bool check_called(theft *t, struct theft_propfun_info *info,
        void **args, void *env) {
    theft_hash buffer[THEFT_MAX_ARITY];
    get_arg_hash_buffer(buffer, info, args, env);
    return theft_bloom_check(t->bloom, (uint8_t *)buffer,
        info->arity * sizeof(theft_hash));
}

/* Print info about a failure. */
static theft_progress_callback_res report_on_failure(theft *t,
        struct theft_propfun_info *info,
        struct theft_trial_info *ti, theft_progress_cb *cb, void *env) {
    static theft_progress_callback_res cres;

    int arity = info->arity;
    fprintf(t->out, "\n\n -- Counter-Example: %s\n",
        info->name ? info-> name : "");
    fprintf(t->out, "    Trial %u, Seed 0x%016llx\n", ti->trial,
        (uint64_t)ti->seed);
    for (int i = 0; i < arity; i++) {
        theft_print_cb *print = info->type_info[i]->print;
        if (print) {
            fprintf(t->out, "    Argument %d:\n", i);
            print(t->out, ti->args[i], env);
            fprintf(t->out, "\n");
        }
   }

    cres = cb(ti, env);
    return cres;
}