File size: 5,495 Bytes
3afc977
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
//! Generate → execute → filter → record. The data engine's main loop. Each kept
//! program is independently reproducible from (master seed, per-program seed).

use std::collections::hash_map::DefaultHasher;
use std::collections::HashSet;
use std::fs::{self, File};
use std::hash::{Hash, Hasher};
use std::io::{BufWriter, Write};
use std::path::Path;

use anyhow::{Context, Result};
use rand::{RngCore, SeedableRng};
use rand_chacha::ChaCha20Rng;

use crate::grammar::{GenConfig, Generator};
use crate::mutate;
use crate::sandbox::Sandbox;
use crate::spec::{self, Features, Record, RejectStats};
use crate::verify::{self, Verdict};

/// Mutation-gate parameters.
const N_PROBES: usize = 24;
const MAX_ADDED_TESTS: usize = 8;

pub struct GenParams {
    pub count: usize,
    pub seed: u64,
    pub fixed_difficulty: Option<u8>,
    pub n_examples: usize,
    pub n_tests: usize,
    pub budget: i64,
    pub out: String,
    /// Run the mutation gate (strengthen tests). Off for fast bulk training data.
    pub gate: bool,
}

pub fn run(p: GenParams) -> Result<()> {
    if let Some(parent) = Path::new(&p.out).parent() {
        if !parent.as_os_str().is_empty() {
            fs::create_dir_all(parent).ok();
        }
    }
    let file = File::create(&p.out).with_context(|| format!("creating {}", p.out))?;
    let mut w = BufWriter::new(file);

    let sb = Sandbox::new().map_err(|e| anyhow::anyhow!("init sandbox: {e}"))?;
    let mut master = ChaCha20Rng::seed_from_u64(p.seed);

    let mut stats = RejectStats::default();
    let mut kept = 0usize;
    let mut attempts = 0usize;
    let mut self_verify_fail = 0usize;
    let mut seen: HashSet<u64> = HashSet::new();
    let mut total_added_tests = 0usize;
    let mut total_mutants = 0usize;
    let mut total_distinct = 0usize;
    let max_attempts = p.count.saturating_mul(50).max(1000);

    while kept < p.count && attempts < max_attempts {
        attempts += 1;
        let diff = p
            .fixed_difficulty
            .unwrap_or(((attempts - 1) % 5) as u8);
        let prog_seed = master.next_u64();
        let mut prng = ChaCha20Rng::seed_from_u64(prog_seed);

        let cfg = GenConfig::for_difficulty(diff);
        let prog = Generator::new(&mut prng, cfg).generate();

        // Uniqueness: never keep the same source twice. Critical for the
        // data-efficiency study, which controls how many UNIQUE programs exist.
        let mut hasher = DefaultHasher::new();
        prog.source.hash(&mut hasher);
        if !seen.insert(hasher.finish()) {
            stats.duplicate += 1;
            continue;
        }

        let extracted = spec::extract(
            &sb, &mut prng, &prog, p.n_examples, p.n_tests, p.budget, &mut stats,
        );
        let (examples, tests) = match extracted {
            Some(x) => x,
            None => continue,
        };

        // Invariant: the reference program must satisfy its own held-out tests.
        if verify::verify(&sb, &prog.source, &tests, p.budget) != Verdict::Pass {
            self_verify_fail += 1;
            continue;
        }

        // Mutation gate: strengthen tests until they catch every behaviorally
        // distinct mutant. Reject if that can't be done within the cap. Skipped
        // for bulk training data (--no-gate), where test strength doesn't matter.
        let tests = if p.gate {
            let probes = spec::sample_inputs(&prog.params, N_PROBES, &mut prng);
            match mutate::strengthen(&sb, &prog.source, tests, &probes, p.budget, MAX_ADDED_TESTS) {
                Some(g) => {
                    total_added_tests += g.added;
                    total_mutants += g.mutants_total;
                    total_distinct += g.mutants_distinct;
                    g.tests
                }
                None => {
                    stats.weak_tests += 1;
                    continue;
                }
            }
        } else {
            tests
        };

        let infill = spec::make_infill(&prog.source, &mut prng);
        let params = prog.params.iter().map(|t| spec::type_name(*t).to_string()).collect();
        let output = examples.first().map(|e| e.output.clone()).unwrap_or(crate::value::LValue::Nil);
        let features = Features::build(&prog.source, &prog.features, &output);

        let rec = Record {
            id: format!("echo1-{:08}", kept),
            difficulty: diff,
            seed: prog_seed,
            source: prog.source,
            params,
            examples,
            tests,
            infill,
            features,
        };

        let line = serde_json::to_string(&rec)?;
        w.write_all(line.as_bytes())?;
        w.write_all(b"\n")?;
        kept += 1;
    }

    w.flush()?;

    eprintln!("kept       {kept} / requested {}", p.count);
    eprintln!("attempts   {attempts}");
    eprintln!("rejects    load_fail={} run_fail={} nonobservable={} nondet={} noop/sparse={} dup={} weak_tests={}",
        stats.load_fail, stats.run_fail, stats.nonobservable, stats.nondeterministic, stats.noop, stats.duplicate, stats.weak_tests);
    eprintln!("self_verify_fail {self_verify_fail} (should be 0)");
    eprintln!("mutation gate: {total_mutants} mutants, {total_distinct} behaviorally distinct, {total_added_tests} discriminating tests added across {kept} kept programs");
    eprintln!("wrote      {}", p.out);

    if kept < p.count {
        eprintln!("WARN: hit attempt cap before reaching requested count");
    }
    Ok(())
}