//! Restricted-grammar generator for pure-Lua functions. //! //! Emits a single global `function f(...)` over typed parameters. Generation is //! type-aware so that programs run cleanly instead of mostly raising //! nil-arithmetic / index errors — the execution filter then keeps only the //! terminating, observable ones. Difficulty knobs (DATA.md) are the axes along //! which global structure grows: nesting depth, def-use count, length, table //! size, loop bound. use std::collections::HashSet; use rand::Rng; use rand_chacha::ChaCha20Rng; #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum Type { Number, Bool, List, // list of Number Str, } /// Structural features of a generated program, recorded as metadata so eval can /// stratify and the data-efficiency study can group by construct (EXPERIMENTS.md). #[derive(Debug, Clone, Default)] pub struct ProgramFeatures { pub recursion: bool, pub closure: bool, pub loops: bool, pub table_build: bool, } #[derive(Debug, Clone)] pub struct GenConfig { pub n_params: usize, pub n_locals: usize, pub max_depth: usize, pub expr_fuel: usize, pub list_len: usize, pub loop_bound: i64, } impl GenConfig { /// Difficulty 0..=4 maps onto concrete knob settings. pub fn for_difficulty(d: u8) -> GenConfig { match d { 0 => GenConfig { n_params: 1, n_locals: 1, max_depth: 1, expr_fuel: 2, list_len: 3, loop_bound: 4 }, 1 => GenConfig { n_params: 2, n_locals: 2, max_depth: 2, expr_fuel: 3, list_len: 4, loop_bound: 6 }, 2 => GenConfig { n_params: 2, n_locals: 3, max_depth: 3, expr_fuel: 4, list_len: 5, loop_bound: 8 }, 3 => GenConfig { n_params: 3, n_locals: 4, max_depth: 4, expr_fuel: 5, list_len: 6, loop_bound: 10 }, _ => GenConfig { n_params: 3, n_locals: 5, max_depth: 5, expr_fuel: 6, list_len: 8, loop_bound: 12 }, } } } /// A generated program plus the parameter types needed to sample inputs. pub struct Program { pub source: String, pub params: Vec, pub features: ProgramFeatures, } pub struct Generator<'a> { rng: &'a mut ChaCha20Rng, cfg: GenConfig, scope: Vec<(String, Type)>, /// Names whose value derives (transitively) from a parameter. Used to keep /// returns input-dependent so they aren't constant (the noop filter). tainted: HashSet, feat: ProgramFeatures, next_id: usize, } impl<'a> Generator<'a> { pub fn new(rng: &'a mut ChaCha20Rng, cfg: GenConfig) -> Self { Generator { rng, cfg, scope: Vec::new(), tainted: HashSet::new(), feat: ProgramFeatures::default(), next_id: 0, } } pub fn generate(&mut self) -> Program { self.scope.clear(); self.tainted.clear(); self.feat = ProgramFeatures::default(); self.next_id = 0; // Parameters: at least one Number, the rest random typed. Every // parameter is, by definition, input-dependent (tainted). let mut params = Vec::new(); for i in 0..self.cfg.n_params { let ty = if i == 0 { Type::Number } else { self.rand_type() }; let name = self.fresh(); self.tainted.insert(name.clone()); self.scope.push((name, ty)); params.push(ty); } let param_names: Vec = self.scope.iter().map(|(n, _)| n.clone()).collect(); let mut lines: Vec = Vec::new(); // Global-structure features that def-use chains alone don't give. Higher // difficulty only; each is the kind of long-range dependency AR pays for // and bidirectional refinement is meant to exploit (DATA.md). if self.cfg.max_depth >= 3 && self.rng.gen_bool(0.6) { self.gen_recursive_helper(&mut lines, 1); } // A closure capturing a local declared earlier: a long-range binding. if self.cfg.max_depth >= 2 && self.rng.gen_bool(0.5) { self.gen_closure_helper(&mut lines, 1); } // A table built incrementally where each element depends on the previous // one: table coherence + a def-use chain threaded through the table. if self.cfg.max_depth >= 2 && self.rng.gen_bool(0.5) { self.gen_table_build(&mut lines, 1); } let n_locals = self.cfg.n_locals; for _ in 0..n_locals { self.gen_local(&mut lines, 1); } // Control-flow body adds nesting / global structure. self.gen_block(&mut lines, 1, self.cfg.max_depth); // Final return, anchored on an input-dependent value. let ret = self.gen_return(1); lines.push(ret); let mut src = format!("function f({})\n", param_names.join(", ")); for l in &lines { src.push_str(l); src.push('\n'); } src.push_str("end\n"); Program { source: src, params, features: self.feat.clone() } } fn fresh(&mut self) -> String { self.next_id += 1; format!("v{}", self.next_id) } fn rand_type(&mut self) -> Type { match self.rng.gen_range(0..4) { 0 => Type::Number, 1 => Type::Bool, 2 => Type::List, _ => Type::Str, } } fn vars_of(&self, ty: Type) -> Vec { self.scope .iter() .filter(|(_, t)| *t == ty) .map(|(n, _)| n.clone()) .collect() } /// Does an emitted expression reference any input-dependent variable? /// Tokenizes on identifier boundaries so `v1` doesn't match `v10`. fn refs_tainted(&self, s: &str) -> bool { let mut cur = String::new(); for ch in s.chars().chain(std::iter::once(' ')) { if ch.is_alphanumeric() || ch == '_' { cur.push(ch); } else if !cur.is_empty() { if self.tainted.contains(&cur) { return true; } cur.clear(); } } false } /// A randomly chosen tainted Number variable. Parameter 0 is always one, so /// this never fails after parameters are in scope. fn anchor_num(&mut self) -> String { let cands: Vec = self .scope .iter() .filter(|(n, t)| *t == Type::Number && self.tainted.contains(n)) .map(|(n, _)| n.clone()) .collect(); self.pick(&cands).unwrap_or_else(|| "0".to_string()) } fn indent(depth: usize) -> String { " ".repeat(depth) } // ---- statements ---- fn gen_local(&mut self, lines: &mut Vec, depth: usize) { let ty = self.rand_type(); let name = self.fresh(); let rhs = self.gen_expr(ty, self.cfg.expr_fuel); if self.refs_tainted(&rhs) { self.tainted.insert(name.clone()); } lines.push(format!("{}local {} = {}", Self::indent(depth), name, rhs)); self.scope.push((name, ty)); } fn gen_block(&mut self, lines: &mut Vec, depth: usize, fuel: usize) { // At the fuel floor emit a single leaf statement rather than nothing, so // control-flow bodies are never empty (no dead `if ... then end` in the // training data). if fuel == 0 { if self.rng.gen_bool(0.5) { self.gen_assign(lines, depth); } else { self.gen_local(lines, depth); } return; } let n_stmts = self.rng.gen_range(1..=2 + fuel.min(2)); for _ in 0..n_stmts { match self.rng.gen_range(0..4) { 0 => self.gen_local(lines, depth), 1 => self.gen_assign(lines, depth), 2 => self.gen_if(lines, depth, fuel), _ => self.gen_for(lines, depth, fuel), } } } fn gen_assign(&mut self, lines: &mut Vec, depth: usize) { // Reassign an existing Number var to build a def-use chain. let nums = self.vars_of(Type::Number); if let Some(name) = self.pick(&nums) { let rhs = self.gen_expr(Type::Number, self.cfg.expr_fuel); if self.refs_tainted(&rhs) { self.tainted.insert(name.clone()); } lines.push(format!("{}{} = {}", Self::indent(depth), name, rhs)); } else { self.gen_local(lines, depth); } } fn gen_if(&mut self, lines: &mut Vec, depth: usize, fuel: usize) { let cond = self.gen_expr(Type::Bool, self.cfg.expr_fuel); lines.push(format!("{}if {} then", Self::indent(depth), cond)); let mark = self.scope.len(); self.gen_block(lines, depth + 1, fuel - 1); self.scope.truncate(mark); // locals leave scope at block end if self.rng.gen_bool(0.5) { lines.push(format!("{}else", Self::indent(depth))); self.gen_block(lines, depth + 1, fuel - 1); self.scope.truncate(mark); } lines.push(format!("{}end", Self::indent(depth))); } fn gen_for(&mut self, lines: &mut Vec, depth: usize, fuel: usize) { // Accumulator pattern: declare a Number before the loop, mutate inside. let acc = self.fresh(); lines.push(format!("{}local {} = 0", Self::indent(depth), acc)); self.scope.push((acc.clone(), Type::Number)); self.feat.loops = true; let n = self.rng.gen_range(1..=self.cfg.loop_bound); let i = self.fresh(); lines.push(format!("{}for {} = 1, {} do", Self::indent(depth), i, n)); let mark = self.scope.len(); self.scope.push((i.clone(), Type::Number)); // Body always advances the accumulator. let inc = self.gen_expr(Type::Number, self.cfg.expr_fuel.min(3)); if self.refs_tainted(&inc) { self.tainted.insert(acc.clone()); } lines.push(format!("{}{} = {} + ({})", Self::indent(depth + 1), acc, acc, inc)); self.gen_block(lines, depth + 1, fuel.saturating_sub(2)); self.scope.truncate(mark); lines.push(format!("{}end", Self::indent(depth))); } fn gen_return(&mut self, depth: usize) -> String { let fuel = self.cfg.expr_fuel; let e = match self.rng.gen_range(0..4) { // String: reuse a tainted string, else concatenate an input-dependent // number's text with more string computation. 3 => { let tstrs: Vec = self .scope .iter() .filter(|(n, t)| *t == Type::Str && self.tainted.contains(n)) .map(|(n, _)| n.clone()) .collect(); if !tstrs.is_empty() && self.rng.gen_bool(0.5) { self.pick(&tstrs).unwrap() } else { let a = self.anchor_num(); format!("(tostring({}) .. {})", a, self.gen_str(fuel.min(2))) } } // Bool: comparison anchored on an input-dependent number. 0 => { let a = self.anchor_num(); let cmp = ["==", "~=", "<", "<=", ">", ">="][self.rng.gen_range(0..6)]; format!("({} {} ({}))", a, cmp, self.gen_num(fuel)) } // List: reuse a tainted list, else a literal that includes a tainted number. 1 => { let tlists: Vec = self .scope .iter() .filter(|(n, t)| *t == Type::List && self.tainted.contains(n)) .map(|(n, _)| n.clone()) .collect(); if !tlists.is_empty() && self.rng.gen_bool(0.5) { self.pick(&tlists).unwrap() } else { let a = self.anchor_num(); let extra = self.rng.gen_range(0..self.cfg.list_len.max(1)); let mut elems = vec![a]; for _ in 0..extra { elems.push(self.gen_num(fuel.min(2))); } format!("{{{}}}", elems.join(", ")) } } // Number: combine an input-dependent number with more computation. _ => { let a = self.anchor_num(); let op = ["+", "-", "*"][self.rng.gen_range(0..3)]; format!("({} {} ({}))", a, op, self.gen_num(fuel)) } }; format!("{}return {}", Self::indent(depth), e) } /// Emit a well-founded recursive helper and bind its result to a tainted /// Number local. The argument is `math.abs() % bound`, so it /// is non-negative and bounded; the `n <= 0` base case guarantees /// termination in at most `bound` calls. fn gen_recursive_helper(&mut self, lines: &mut Vec, depth: usize) { self.feat.recursion = true; let g = self.fresh(); // function name; deliberately not added to scope let base = self.rng.gen_range(0..=3); let op = ["+", "-", "*"][self.rng.gen_range(0..3)]; let ind = Self::indent(depth); let ind1 = Self::indent(depth + 1); lines.push(format!("{ind}local function {g}(n)")); lines.push(format!("{ind1}if n <= 0 then return {base} end")); lines.push(format!("{ind1}return (n {op} {g}((n) - 1))")); lines.push(format!("{ind}end")); let bound = self.cfg.loop_bound.max(2); let arg = self.anchor_num(); let res = self.fresh(); lines.push(format!( "{ind}local {res} = {g}((math.abs({arg}) % {bound}))" )); self.tainted.insert(res.clone()); self.scope.push((res, Type::Number)); } /// Emit a closure that captures a Number local declared earlier (a long-range /// binding: the captured variable lives outside the closure body). Bind its /// result to a tainted Number local. fn gen_closure_helper(&mut self, lines: &mut Vec, depth: usize) { let captured = self.anchor_num(); // a tainted Number to close over let c = self.fresh(); // closure name; not added to the value scope let op = ["+", "-", "*"][self.rng.gen_range(0..3)]; let ind = Self::indent(depth); let ind1 = Self::indent(depth + 1); lines.push(format!("{ind}local function {c}(y)")); lines.push(format!("{ind1}return ({captured} {op} y)")); lines.push(format!("{ind}end")); self.feat.closure = true; let arg = self.gen_num(self.cfg.expr_fuel.min(3)); let res = self.fresh(); let tainted = self.refs_tainted(&arg); // captured is tainted anyway lines.push(format!("{ind}local {res} = {c}({arg})")); if tainted || self.tainted.contains(&captured) { self.tainted.insert(res.clone()); } self.scope.push((res, Type::Number)); } /// Build a table where each element depends on the previous one (table /// coherence). Threads a def-use chain through the table and binds it as a /// tainted List. fn gen_table_build(&mut self, lines: &mut Vec, depth: usize) { self.feat.table_build = true; let t = self.fresh(); let ind = Self::indent(depth); let ind1 = Self::indent(depth + 1); let seed = self.anchor_num(); // first element input-dependent lines.push(format!("{ind}local {t} = {{{seed}}}")); let n = self.rng.gen_range(2..=self.cfg.loop_bound.max(2)); let i = self.fresh(); let op = ["+", "-", "*"][self.rng.gen_range(0..3)]; let step = self.rng.gen_range(1..=4); lines.push(format!("{ind}for {i} = 2, {n} do")); // Each element is a function of its predecessor: t[i] = (t[i-1] op step). lines.push(format!("{ind1}{t}[{i}] = ({t}[{i} - 1] {op} {step})")); lines.push(format!("{ind}end")); self.tainted.insert(t.clone()); self.scope.push((t, Type::List)); } // ---- expressions (type-safe) ---- fn pick(&mut self, opts: &[String]) -> Option { if opts.is_empty() { None } else { let idx = self.rng.gen_range(0..opts.len()); Some(opts[idx].clone()) } } fn gen_expr(&mut self, ty: Type, fuel: usize) -> String { match ty { Type::Number => self.gen_num(fuel), Type::Bool => self.gen_bool(fuel), Type::List => self.gen_list(fuel), Type::Str => self.gen_str(fuel), } } fn gen_num(&mut self, fuel: usize) -> String { if fuel == 0 || self.rng.gen_bool(0.35) { // terminal let vars = self.vars_of(Type::Number); if !vars.is_empty() && self.rng.gen_bool(0.7) { return self.pick(&vars).unwrap(); } return format!("{}", self.rng.gen_range(-9..=9)); } match self.rng.gen_range(0..7) { 0 => { let op = ["+", "-", "*"][self.rng.gen_range(0..3)]; format!("({} {} {})", self.gen_num(fuel - 1), op, self.gen_num(fuel - 1)) } // Parenthesize the operand: `-` on a negative literal would emit // `--4`, which Lua lexes as a comment. 1 => format!("(-({}))", self.gen_num(fuel - 1)), 2 => { // division by a nonzero literal keeps it total let d = self.nonzero(); format!("({} // {})", self.gen_num(fuel - 1), d) } 3 => { let d = self.nonzero(); format!("({} % {})", self.gen_num(fuel - 1), d) } 4 => { // length or safe index of a list, if available let lists = self.vars_of(Type::List); if let Some(l) = self.pick(&lists) { if self.rng.gen_bool(0.5) { format!("#{}", l) } else { // wrap index into 1..#l (l is always non-empty by construction) let idx = self.gen_num(fuel - 1); format!("{}[(math.abs({}) % #{}) + 1]", l, idx, l) } } else { self.gen_num(fuel - 1) } } 5 => { // string length, a cross-type dependency string -> number let strs = self.vars_of(Type::Str); if let Some(s) = self.pick(&strs) { format!("#{}", s) } else { self.gen_num(fuel - 1) } } _ => { let fns = ["math.abs", "math.floor", "math.ceil"]; let f = fns[self.rng.gen_range(0..fns.len())]; format!("{}({})", f, self.gen_num(fuel - 1)) } } } fn gen_str(&mut self, fuel: usize) -> String { if fuel == 0 || self.rng.gen_bool(0.4) { let vars = self.vars_of(Type::Str); if !vars.is_empty() && self.rng.gen_bool(0.6) { return self.pick(&vars).unwrap(); } return self.str_literal(); } match self.rng.gen_range(0..4) { 0 => format!("({} .. {})", self.gen_str(fuel - 1), self.gen_str(fuel - 1)), 1 => { // string.sub is total: out-of-range indices yield "". let a = self.rng.gen_range(1..=4); let b = self.rng.gen_range(a..=a + 4); format!("string.sub({}, {}, {})", self.gen_str(fuel - 1), a, b) } 2 => { // bounded repetition to avoid string blow-up let k = self.rng.gen_range(0..=3); format!("string.rep({}, {})", self.gen_str(fuel - 1), k) } _ => { let f = ["string.upper", "string.lower", "string.reverse"] [self.rng.gen_range(0..3)]; format!("{}({})", f, self.gen_str(fuel - 1)) } } } fn str_literal(&mut self) -> String { // Fixed small pool: keeps the token-level string vocabulary tiny and // closed (no unseen strings at eval, which would break reconstruction). const POOL: &[&str] = &[ "a", "b", "ab", "abc", "x", "y", "hi", "ok", "lua", "foo", "bar", "baz", "key", "val", "one", "two", ]; format!("\"{}\"", POOL[self.rng.gen_range(0..POOL.len())]) } fn gen_bool(&mut self, fuel: usize) -> String { if fuel == 0 || self.rng.gen_bool(0.3) { let vars = self.vars_of(Type::Bool); if !vars.is_empty() && self.rng.gen_bool(0.5) { return self.pick(&vars).unwrap(); } return if self.rng.gen_bool(0.5) { "true".into() } else { "false".into() }; } match self.rng.gen_range(0..3) { 0 => { let op = ["==", "~=", "<", "<=", ">", ">="][self.rng.gen_range(0..6)]; format!("({} {} {})", self.gen_num(fuel - 1), op, self.gen_num(fuel - 1)) } 1 => { let op = ["and", "or"][self.rng.gen_range(0..2)]; format!("({} {} {})", self.gen_bool(fuel - 1), op, self.gen_bool(fuel - 1)) } _ => format!("(not {})", self.gen_bool(fuel - 1)), } } fn gen_list(&mut self, fuel: usize) -> String { let vars = self.vars_of(Type::List); if !vars.is_empty() && self.rng.gen_bool(0.5) { return self.pick(&vars).unwrap(); } // Non-empty literal so that indexing/# are always safe. let len = 1 + self.rng.gen_range(0..self.cfg.list_len.max(1)); let elems: Vec = (0..len) .map(|_| self.gen_num(fuel.saturating_sub(1).min(2))) .collect(); format!("{{{}}}", elems.join(", ")) } fn nonzero(&mut self) -> i64 { let mut d = self.rng.gen_range(-9..=9); if d == 0 { d = 1; } d } }