diff --git a/.github/workflows/build-test.yml b/.github/workflows/build-test.yml index 34cbcdc6..9101b5d0 100644 --- a/.github/workflows/build-test.yml +++ b/.github/workflows/build-test.yml @@ -204,7 +204,7 @@ jobs: - name: Run tests if: startsWith(matrix.os, 'ubuntu') && startsWith(matrix.python, '3.8') - run: cargo test + run: cargo test --features=fuzz - name: Exhaustive assign tests if: startsWith(matrix.os, 'ubuntu') && startsWith(matrix.python, '3.8') diff --git a/Cargo.toml b/Cargo.toml index 1a91688f..cfd47ea7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -38,6 +38,9 @@ yaml-rust = "0.4" linked-hash-map = "0.5.6" serde = { version = "1.0", features = ["derive", "rc"] } regex = "1.8.4" +rand = { version = "0.8.5", optional = true } +rand_chacha = { version = "0.3.1", optional = true } +lfsr = { version = "0.3.0", optional = true } [dependencies.pyo3] version = "0.20.2" @@ -55,6 +58,7 @@ crate-type = ["cdylib", "rlib"] [features] extension-module = ["dep:pyo3"] +fuzz = ["dep:rand", "dep:rand_chacha", "dep:lfsr"] default = [] [target.'cfg(target_family="wasm")'.dependencies] diff --git a/src/compiler/fuzz.rs b/src/compiler/fuzz.rs new file mode 100644 index 00000000..9dd16831 --- /dev/null +++ b/src/compiler/fuzz.rs @@ -0,0 +1,191 @@ +use rand::prelude::*; +use std::rc::Rc; + +/// A trait that conveys the types of various things needed by this kind of fuzz +/// generation. +/// +/// Tag is the type of step identifiers used during expansion. Each node in the +/// generic tree this generator creates is either concrete or contains a tag and +/// an index which gives it a unique value. +/// +/// Expr is the type of value being generated in this fuzz tree, and what will +/// ultimately result from generation. It must be able to contain a value that +/// is recognizable as containing a Tag and index or something that has been +/// fully generated and needs nothing else. +/// +/// Error is the type of error that can be throw while generating an Expr tree. +/// It should be str convertible so we can add context. +/// +/// State is the mutable part of expression generation where needed context is +/// held. All rules will receive a mutable State reference to record information +/// about what they're doing so it can be referred to later during expression +/// generation or after. +/// +/// As an example of State use, imagine that this fuzzer is creating an arithmetic +/// expression. State might remember the names of randomly generated variables in +/// an expression so that they can be given values when the expression is used, or +/// separately remember what operations are being applied so a test can +/// calculate the expected result on its own before trying something that evaluates +/// the expression separately so some other process of evaluation can be checked. +pub trait FuzzTypeParams { + type Tag: Eq + Clone; + type Expr: Eq + Clone + ExprModifier; + type Error: for<'a> From<&'a str>; + type State; +} + +#[derive(Clone, Debug)] +/// Specifies to the fuzz engine an unfinished node from an expression tree. The +/// actual way this is represented in the tree type is situational so this type +/// allows the user to determine that. Expr will be an Expr from FuzzTypeParams +/// and Tag will be a Tag from FuzzTypeParams. +pub struct FuzzChoice { + pub tag: Tag, + pub atom: Expr, +} + +/// This trait is provided for a specific Expr type that can be generated by this +/// fuzz system. These methods allow the system to search a generated expression +/// for incomplete nodes, specify a replacement generated by a rule and capture the +/// path to a node to be given to the rules during generation. +pub trait ExprModifier { + type Tag; + type Expr; + + /// Add identified in-progress expansions into waiters. + /// These are used as expansion candidates during each step of generation. + /// Each will be tried in a random order with all rules in a random order until + /// one of the rules returns a replacement. + fn find_waiters(&self, waiters: &mut Vec>); + + /// Replace a value where it appears in the structure with a new value. + fn replace_node(&self, to_replace: &Self::Expr, new_value: Self::Expr) -> Self::Expr; + + /// Find the expression in the target structure and give the path down to it + /// expressed as a snapshot of the traversed nodes. + fn find_in_structure(&self, target: &Self::Expr) -> Option>; +} + +/// This is the main active part the user provides for this system. Each rule is +/// given a chance to provide a replacement for some incomplete part of the +/// expression tree. The replacement can itself contain incomplete parts which +/// will be evaluated later, thus rules can be modular, recognizing some tag and +/// generating a partial expansion which can then be completed by some combination +/// of other rule evaluations. +/// +/// State is a mutable reference to a common state shared by the rules to +/// accumulate information about the result expression. +/// +/// Terminate is set by the user after some target complexity or number of +/// iterations is passed. Rules should respond to terminate by generating the +/// smallest possible expansion or refusing expansion if some other rule provides +/// a smaller alternative. +/// +/// Parents is a list which provides in order the Expr nodes which are one step +/// closer to the root than the last (with the implied last one being the one +/// containing Tag). +pub trait Rule { + fn check( + &self, + state: &mut FT::State, + tag: &FT::Tag, + idx: usize, + terminate: bool, + parents: &[FT::Expr], + ) -> Result, FT::Error>; +} + +/// The fuzz generator object. It can be stepped to generate an Expr according to +/// a given set of Rules. It uses the methods of the ExprModifier trait carried +/// by the Expr type to obtain the incomplete nodes, determine the path to each, +/// and replace them with the nodes given by rules when those rules choose to +/// provide them. It starts with a single incomplete node and generates a tree +/// of complete nodes. +pub struct FuzzGenerator { + idx: usize, + structure: FT::Expr, + waiting: Vec>, + rules: Vec>>, +} + +impl FuzzGenerator { + /// Given a possibly incomplete root Expr node and a set of Rules, initialize + /// a fuzzer so we can step it until it completes or we decide to stop. + pub fn new(node: FT::Expr, rules: &[Rc>]) -> Self { + let mut waiting = Vec::new(); + node.find_waiters(&mut waiting); + FuzzGenerator { + idx: 1, + structure: node, + waiting, + rules: rules.to_vec(), + } + } + + /// Get the current Expr tree even if it contains incomplete nodes. + pub fn result(&self) -> &FT::Expr { + &self.structure + } + + fn remove_waiting(&mut self, waiting_atom: &FT::Expr) -> Result<(), FT::Error> { + if let Some(pos) = self.waiting.iter().position(|w| w.atom == *waiting_atom) { + self.waiting.remove(pos); + Ok(()) + } else { + Err("remove_waiting must succeed".into()) + } + } + + /// Perform one step of expansion of the Expr tree, updating State. + /// The result will be Ok(true) if the tree is complete, Ok(false) if expansion + /// succeeded but wasn't complete, or Error if something failed. + /// + /// Give terminate if the result() Expr has sufficient complexity or the + /// process is taking too long to tell the rules to stop expanding as much as + /// possible. + pub fn expand( + &mut self, + state: &mut FT::State, + terminate: bool, + rng: &mut R, + ) -> Result { + let mut waiting = self.waiting.clone(); + + while !waiting.is_empty() { + let mut rules = self.rules.clone(); + let waiting_choice: usize = rng.gen::() % waiting.len(); + + let chosen = waiting[waiting_choice].clone(); + waiting.remove(waiting_choice); + + let heritage = if let Some(heritage) = self.structure.find_in_structure(&chosen.atom) { + heritage + } else { + return Err("Parity wasn't kept between the structure and waiting list".into()); + }; + + while !rules.is_empty() { + let rule_choice: usize = rng.gen::() % rules.len(); + let chosen_rule = rules[rule_choice].clone(); + rules.remove(rule_choice); + + if let Some(res) = + chosen_rule.check(state, &chosen.tag, self.idx, terminate, &heritage)? + { + let mut new_waiters = Vec::new(); + res.find_waiters(&mut new_waiters); + for n in new_waiters.into_iter() { + self.idx += 1; + self.waiting.push(n); + } + + self.remove_waiting(&chosen.atom)?; + self.structure = self.structure.replace_node(&chosen.atom, res); + return Ok(!self.waiting.is_empty()); + } + } + } + + Err("rule deadlock".into()) + } +} diff --git a/src/compiler/mod.rs b/src/compiler/mod.rs index 270a2f82..0b835139 100644 --- a/src/compiler/mod.rs +++ b/src/compiler/mod.rs @@ -18,6 +18,8 @@ pub mod debug; pub mod dialect; pub mod evaluate; pub mod frontend; +#[cfg(any(test, feature = "fuzz"))] +pub mod fuzz; pub mod gensym; mod inline; mod lambda; diff --git a/src/compiler/sexp.rs b/src/compiler/sexp.rs index 0edea62d..46adc013 100644 --- a/src/compiler/sexp.rs +++ b/src/compiler/sexp.rs @@ -19,6 +19,8 @@ use serde::Serialize; use crate::classic::clvm::__type_compatibility__::bi_one; use crate::classic::clvm::__type_compatibility__::{bi_zero, Bytes, BytesFromType}; use crate::classic::clvm::casts::{bigint_from_bytes, bigint_to_bytes_clvm, TConvertOption}; +#[cfg(any(test, feature = "fuzz"))] +use crate::compiler::fuzz::{ExprModifier, FuzzChoice}; use crate::compiler::prims::prims; use crate::compiler::srcloc::Srcloc; use crate::util::{number_from_u8, u8_from_number, Number}; @@ -1219,3 +1221,90 @@ where } } } + +// +// Fuzzing support for SExp +// +#[cfg(any(test, feature = "fuzz"))] +fn find_in_structure_inner( + parents: &mut Vec>, + structure: Rc, + target: &Rc, +) -> bool { + if let SExp::Cons(_, a, b) = structure.borrow() { + parents.push(structure.clone()); + if find_in_structure_inner(parents, a.clone(), target) { + return true; + } + if find_in_structure_inner(parents, b.clone(), target) { + return true; + } + + parents.pop(); + } + + structure == *target +} + +#[cfg(any(test, feature = "fuzz"))] +pub fn extract_atom_replacement( + myself: &Expr, + a: &[u8], +) -> Option>> { + if a.starts_with(b"${") && a.ends_with(b"}") { + if let Some(c_idx) = a.iter().position(|&c| c == b':') { + return Some(FuzzChoice { + tag: a[c_idx + 1..a.len() - 1].to_vec(), + atom: myself.clone(), + }); + } + } + + None +} + +#[cfg(any(test, feature = "fuzz"))] +impl ExprModifier for Rc { + type Expr = Self; + type Tag = Vec; + + fn find_waiters(&self, waiters: &mut Vec>) { + match self.borrow() { + SExp::Cons(_, a, b) => { + a.find_waiters(waiters); + b.find_waiters(waiters); + } + SExp::Atom(_, a) => { + if let Some(r) = extract_atom_replacement(self, a) { + waiters.push(r); + } + } + _ => {} + } + } + + fn replace_node(&self, to_replace: &Self::Expr, new_value: Self::Expr) -> Self::Expr { + if let SExp::Cons(l, a, b) = self.borrow() { + let new_a = a.replace_node(to_replace, new_value.clone()); + let new_b = b.replace_node(to_replace, new_value.clone()); + if Rc::as_ptr(&new_a) != Rc::as_ptr(a) || Rc::as_ptr(&new_b) != Rc::as_ptr(b) { + return Rc::new(SExp::Cons(l.clone(), new_a, new_b)); + } + } + + if self == to_replace { + return new_value; + } + + self.clone() + } + + fn find_in_structure(&self, target: &Self::Expr) -> Option> { + let mut parents = Vec::new(); + if find_in_structure_inner(&mut parents, self.clone(), target) { + Some(parents) + } else { + None + } + } +} diff --git a/src/tests/classic/run.rs b/src/tests/classic/run.rs index cbf09ab8..3e4a4834 100644 --- a/src/tests/classic/run.rs +++ b/src/tests/classic/run.rs @@ -5,8 +5,6 @@ use rand::distributions::Standard; #[cfg(test)] use rand::prelude::*; #[cfg(test)] -use rand::Rng; -#[cfg(test)] use rand_chacha::ChaChaRng; use std::borrow::Borrow; diff --git a/src/tests/compiler/clvm.rs b/src/tests/compiler/clvm.rs index e7542ad5..f71c1536 100644 --- a/src/tests/compiler/clvm.rs +++ b/src/tests/compiler/clvm.rs @@ -1,8 +1,6 @@ #[cfg(test)] use rand::prelude::*; #[cfg(test)] -use rand::Rng; -#[cfg(test)] use rand_chacha::ChaChaRng; use num_bigint::ToBigInt; diff --git a/src/tests/compiler/fuzz.rs b/src/tests/compiler/fuzz.rs new file mode 100644 index 00000000..764a8293 --- /dev/null +++ b/src/tests/compiler/fuzz.rs @@ -0,0 +1,497 @@ +use num_bigint::ToBigInt; + +use rand::distributions::Standard; +use rand::prelude::Distribution; +use rand::{Rng, SeedableRng}; +use rand_chacha::ChaCha8Rng; +use std::borrow::Borrow; +use std::collections::{BTreeSet, HashMap}; +use std::fmt::Debug; +use std::rc::Rc; + +use clvmr::Allocator; + +use crate::classic::clvm_tools::stages::stage_0::{DefaultProgramRunner, TRunProgram}; +use crate::compiler::clvm::run; +use crate::compiler::compiler::DefaultCompilerOpts; +use crate::compiler::comptypes::{BodyForm, CompileErr, CompilerOpts}; +use crate::compiler::fuzz::{ExprModifier, FuzzChoice, FuzzGenerator, FuzzTypeParams, Rule}; +use crate::compiler::prims::primquote; +use crate::compiler::sexp::{enlist, extract_atom_replacement, parse_sexp, SExp}; +use crate::compiler::srcloc::Srcloc; + +#[derive(Debug)] +pub struct GenError { + pub message: String, +} +impl From<&str> for GenError { + fn from(m: &str) -> GenError { + GenError { + message: m.to_string(), + } + } +} + +pub fn compose_sexp(loc: Srcloc, s: &str) -> Rc { + parse_sexp(loc, s.bytes()).expect("should parse")[0].clone() +} + +pub fn simple_run( + opts: Rc, + expr: Rc, + env: Rc, +) -> Result, CompileErr> { + let mut allocator = Allocator::new(); + let runner: Rc = Rc::new(DefaultProgramRunner::new()); + Ok(run( + &mut allocator, + runner, + opts.prim_map(), + expr, + env, + None, + None, + )?) +} + +pub fn simple_seeded_rng(seed: u32) -> ChaCha8Rng { + let mut seed_data: [u8; 32] = [1; 32]; + for i in 16..28 { + seed_data[i] = 2; + } + seed_data[28] = ((seed >> 24) & 0xff) as u8; + seed_data[29] = ((seed >> 16) & 0xff) as u8; + seed_data[30] = ((seed >> 8) & 0xff) as u8; + seed_data[31] = (seed & 0xff) as u8; + ChaCha8Rng::from_seed(seed_data) +} + +// A generic, simple representation of expressions that allow us to evaluate +// simple expressions. We can add stuff that increases this capability for +// all consumers. +#[derive(Debug, Clone, Eq, PartialEq)] +pub enum SupportedOperators { + Plus, + Minus, + Times, +} + +impl SupportedOperators { + pub fn to_int(&self) -> usize { + match self { + SupportedOperators::Plus => 16, + SupportedOperators::Minus => 17, + SupportedOperators::Times => 18, + } + } + pub fn to_sexp(&self, srcloc: &Srcloc) -> SExp { + SExp::Integer(srcloc.clone(), self.to_int().to_bigint().unwrap()) + } + + pub fn to_bodyform(&self, srcloc: &Srcloc) -> BodyForm { + BodyForm::Value(self.to_sexp(srcloc)) + } +} + +impl Distribution for Standard { + fn sample(&self, rng: &mut R) -> SupportedOperators { + match rng.gen::() % 3 { + 0 => SupportedOperators::Plus, + 1 => SupportedOperators::Minus, + _ => SupportedOperators::Times, + } + } +} + +#[derive(Debug, Clone, Eq, PartialEq)] +pub enum ValueSpecification { + ConstantValue(Rc), + VarRef(Vec), + ClvmBinop( + SupportedOperators, + Rc, + Rc, + ), +} + +pub trait HasVariableStore { + fn get(&self, name: &[u8]) -> Option>; +} + +impl ValueSpecification { + pub fn to_sexp(&self, srcloc: &Srcloc) -> SExp { + match self { + ValueSpecification::ConstantValue(c) => { + let c_borrowed: &SExp = c.borrow(); + c_borrowed.clone() + } + ValueSpecification::VarRef(c) => SExp::Atom(srcloc.clone(), c.clone()), + ValueSpecification::ClvmBinop(op, left, right) => enlist( + srcloc.clone(), + &[ + Rc::new(op.to_sexp(srcloc)), + Rc::new(left.to_sexp(srcloc)), + Rc::new(right.to_sexp(srcloc)), + ], + ), + } + } + + pub fn to_bodyform(&self, srcloc: &Srcloc) -> BodyForm { + match self { + ValueSpecification::ClvmBinop(op, left, right) => BodyForm::Call( + srcloc.clone(), + vec![ + Rc::new(op.to_bodyform(srcloc)), + Rc::new(left.to_bodyform(srcloc)), + Rc::new(right.to_bodyform(srcloc)), + ], + None, + ), + ValueSpecification::ConstantValue(v) => { + let borrowed_sexp: &SExp = v.borrow(); + BodyForm::Quoted(borrowed_sexp.clone()) + } + ValueSpecification::VarRef(v) => { + BodyForm::Value(SExp::Atom(srcloc.clone(), v.to_vec())) + } + } + } + + pub fn get_free_vars<'a>(&'a self) -> BTreeSet> { + let mut stack = vec![Rc::new(self.clone())]; + let mut result = BTreeSet::default(); + + while let Some(v) = stack.pop() { + match v.borrow() { + ValueSpecification::VarRef(c) => { + result.insert(c.clone()); + } + ValueSpecification::ClvmBinop(_, l, r) => { + stack.push(l.clone()); + stack.push(r.clone()); + } + _ => {} + } + } + + result + } + + pub fn interpret( + &self, + opts: Rc, + srcloc: &Srcloc, + value_map: &Store, + ) -> Rc { + match self { + ValueSpecification::ConstantValue(c) => c.clone(), + ValueSpecification::VarRef(c) => { + if let Some(value) = value_map.get(c) { + value.interpret(opts, srcloc, value_map) + } else { + todo!(); + } + } + ValueSpecification::ClvmBinop(op, left, right) => { + let operator = op.to_sexp(srcloc); + let left_val = left.interpret(opts.clone(), srcloc, value_map); + let right_val = right.interpret(opts.clone(), srcloc, value_map); + let nil = Rc::new(SExp::Nil(srcloc.clone())); + let expr = Rc::new(SExp::Cons( + srcloc.clone(), + Rc::new(operator), + Rc::new(SExp::Cons( + srcloc.clone(), + Rc::new(primquote(srcloc.clone(), left_val)), + Rc::new(SExp::Cons( + srcloc.clone(), + Rc::new(primquote(srcloc.clone(), right_val)), + nil.clone(), + )), + )), + )); + simple_run(opts, expr, nil).expect("should succeed") + } + } + } +} + +// +// Fuzzing support for ValueSpecification alone. +// Provided for testing the fuzzer itself and maybe future use in tests. +// +fn find_in_structure_inner( + parents: &mut Vec>, + structure: Rc, + target: &Rc, +) -> bool { + if let ValueSpecification::ClvmBinop(_, l, r) = structure.borrow() { + parents.push(structure.clone()); + if find_in_structure_inner(parents, l.clone(), target) { + return true; + } + if find_in_structure_inner(parents, r.clone(), target) { + return true; + } + + parents.pop(); + } + + structure == *target +} + +impl ExprModifier for Rc { + type Tag = Vec; + type Expr = Rc; + + /// Add identified in-progress expansions into waiters. + /// These are used as expansion candidates during each step of generation. + /// Each will be tried in a random order with all rules in a random order until + /// one of the rules returns a replacement. + fn find_waiters(&self, waiters: &mut Vec>) { + match self.borrow() { + ValueSpecification::VarRef(v) => { + if v.starts_with(b"${") && v.ends_with(b"}") { + if let Some(r) = extract_atom_replacement(self, v) { + waiters.push(r); + } + } + } + ValueSpecification::ClvmBinop(_, l, r) => { + l.find_waiters(waiters); + r.find_waiters(waiters); + } + _ => {} + } + } + + /// Replace a value where it appears in the structure with a new value. + fn replace_node(&self, to_replace: &Self::Expr, new_value: Self::Expr) -> Self::Expr { + if let ValueSpecification::ClvmBinop(op, l, r) = self.borrow() { + let new_l = l.replace_node(to_replace, new_value.clone()); + let new_r = r.replace_node(to_replace, new_value.clone()); + if Rc::as_ptr(&new_l) != Rc::as_ptr(l) || Rc::as_ptr(&new_r) != Rc::as_ptr(r) { + return Rc::new(ValueSpecification::ClvmBinop(op.clone(), new_l, new_r)); + } + } + + if self == to_replace { + return new_value; + } + + self.clone() + } + + /// Find the expression in the target structure and give the path down to it + /// expressed as a snapshot of the traversed nodes. + fn find_in_structure(&self, target: &Self::Expr) -> Option> { + let mut parents = Vec::new(); + if find_in_structure_inner(&mut parents, self.clone(), target) { + Some(parents) + } else { + None + } + } +} + +struct SimpleFuzzItselfTestState { + srcloc: Srcloc, + used_vars: HashMap, Rc>, +} + +impl HasVariableStore for SimpleFuzzItselfTestState { + fn get(&self, name: &[u8]) -> Option> { + self.used_vars.get(name).cloned() + } +} + +struct SimpleFuzzItselfTest; +impl FuzzTypeParams for SimpleFuzzItselfTest { + type Tag = Vec; + type Expr = Rc; + type Error = String; + type State = SimpleFuzzItselfTestState; +} + +struct SimpleRuleVar; +impl Rule for SimpleRuleVar { + fn check( + &self, + state: &mut SimpleFuzzItselfTestState, + _tag: &Vec, + _idx: usize, + _terminate: bool, + _parents: &[Rc], + ) -> Result>, String> { + let n = 1 + state.used_vars.len(); + // Set each v = n + let v1 = format!("v{n}").as_bytes().to_vec(); + state.used_vars.insert( + v1.clone(), + Rc::new(ValueSpecification::ConstantValue(Rc::new(SExp::Atom( + state.srcloc.clone(), + vec![n as u8], + )))), + ); + Ok(Some(Rc::new(ValueSpecification::VarRef(v1)))) + } +} + +struct SimpleRuleOp { + op: SupportedOperators, +} +impl Rule for SimpleRuleOp { + fn check( + &self, + _state: &mut SimpleFuzzItselfTestState, + _tag: &Vec, + idx: usize, + terminate: bool, + _parents: &[Rc], + ) -> Result>, String> { + if terminate { + return Ok(None); + } + + let l = format!("${{{idx}:expand}}").as_bytes().to_vec(); + let r = format!("${{{}:expand}}", idx + 1).as_bytes().to_vec(); + + Ok(Some(Rc::new(ValueSpecification::ClvmBinop( + self.op.clone(), + Rc::new(ValueSpecification::VarRef(l)), + Rc::new(ValueSpecification::VarRef(r)), + )))) + } +} + +#[test] +fn test_compose_sexp() { + let loc = Srcloc::start("*vstest*"); + assert_eq!( + compose_sexp(loc.clone(), "(hi . there)"), + Rc::new(SExp::Cons( + loc.clone(), + Rc::new(SExp::Atom(loc.clone(), b"hi".to_vec())), + Rc::new(SExp::Atom(loc.clone(), b"there".to_vec())) + )) + ); +} + +#[test] +fn test_random_value_spec() { + let mut rng = simple_seeded_rng(11); + let mut state = SimpleFuzzItselfTestState { + srcloc: Srcloc::start("*vstest*"), + used_vars: HashMap::default(), + }; + let topnode = Rc::new(ValueSpecification::VarRef(b"${0:top}".to_vec())); + let rules: Vec>> = vec![ + Rc::new(SimpleRuleVar), + Rc::new(SimpleRuleOp { + op: SupportedOperators::Plus, + }), + Rc::new(SimpleRuleOp { + op: SupportedOperators::Times, + }), + ]; + let mut fuzzer = FuzzGenerator::new(topnode, &rules); + + let mut idx = 0; + while let Ok(true) = fuzzer.expand(&mut state, idx > 5, &mut rng) { + // Repeat + idx += 1; + } + + assert_eq!( + fuzzer.result().to_sexp(&state.srcloc).to_string(), + "(16 (16 (18 v4 (16 v3 v6)) v5) (18 v7 (18 v1 v2)))" + ); + // Since each v = n, the expression comes down to + // (+ (+ (* 4 (+ 3 6)) 5) (* 7 (* 1 2))) = 55 + let opts: Rc = Rc::new(DefaultCompilerOpts::new("*vstest*")); + assert_eq!( + fuzzer + .result() + .interpret(opts, &state.srcloc, &state) + .to_string(), + "55" + ); +} + +struct SimpleFuzzSexpTestState { + srcloc: Srcloc, + count: usize, +} +struct SimpleFuzzSexpTest; + +impl FuzzTypeParams for SimpleFuzzSexpTest { + type Tag = Vec; + type Expr = Rc; + type Error = String; + type State = SimpleFuzzSexpTestState; +} + +struct SimpleRuleAtom; +impl Rule for SimpleRuleAtom { + fn check( + &self, + state: &mut SimpleFuzzSexpTestState, + _tag: &Vec, + _idx: usize, + _terminate: bool, + _parents: &[Rc], + ) -> Result>, String> { + state.count += 1; + Ok(Some(compose_sexp( + state.srcloc.clone(), + &format!("node-{}", state.count), + ))) + } +} + +struct SimpleRuleCons; +impl Rule for SimpleRuleCons { + fn check( + &self, + state: &mut SimpleFuzzSexpTestState, + _tag: &Vec, + idx: usize, + terminate: bool, + _parents: &[Rc], + ) -> Result>, String> { + if terminate { + return Ok(None); + } + + let l = format!("${{{idx}:expand}}").as_bytes().to_vec(); + let r = format!("${{{}:expand}}", idx + 1).as_bytes().to_vec(); + + Ok(Some(Rc::new(SExp::Cons( + state.srcloc.clone(), + Rc::new(SExp::Atom(state.srcloc.clone(), l)), + Rc::new(SExp::Atom(state.srcloc.clone(), r)), + )))) + } +} + +#[test] +fn test_random_sexp() { + let mut rng = simple_seeded_rng(8); + let mut state = SimpleFuzzSexpTestState { + srcloc: Srcloc::start("*vstest*"), + count: 0, + }; + let topnode = Rc::new(SExp::Atom(state.srcloc.clone(), b"${0:top}".to_vec())); + let rules: Vec>> = + vec![Rc::new(SimpleRuleAtom), Rc::new(SimpleRuleCons)]; + let mut fuzzer = FuzzGenerator::new(topnode, &rules); + + let mut idx = 0; + while let Ok(true) = fuzzer.expand(&mut state, idx > 5, &mut rng) { + // Repeat + idx += 1; + } + + assert_eq!(fuzzer.result().to_string(), "((node-3 . node-1) . node-2)"); +} diff --git a/src/tests/compiler/mod.rs b/src/tests/compiler/mod.rs index 4fb8e437..2d7f5764 100644 --- a/src/tests/compiler/mod.rs +++ b/src/tests/compiler/mod.rs @@ -9,6 +9,7 @@ mod cldb; mod clvm; mod compiler; mod evaluate; +mod fuzz; mod optimizer; mod preprocessor; mod repl;