diff --git a/crates/aiken-lang/src/gen_uplc/builder.rs b/crates/aiken-lang/src/gen_uplc/builder.rs index a9189131..3bf84e00 100644 --- a/crates/aiken-lang/src/gen_uplc/builder.rs +++ b/crates/aiken-lang/src/gen_uplc/builder.rs @@ -1181,7 +1181,7 @@ pub fn find_list_clause_or_default_first(clauses: &[TypedClause]) -> &TypedClaus .unwrap_or(&clauses[0]) } -pub fn convert_data_to_type(term: Term, field_type: &Rc) -> Term { +pub fn convert_data_to_type(term: Term, field_type: &Type) -> Term { if field_type.is_int() { Term::un_i_data().apply(term) } else if field_type.is_bytearray() { @@ -1222,7 +1222,7 @@ pub fn convert_data_to_type(term: Term, field_type: &Rc) -> Term, - field_type: &Rc, + field_type: &Type, error_term: Term, ) -> Term { if field_type.is_int() { diff --git a/crates/aiken-lang/src/tipo/infer.rs b/crates/aiken-lang/src/tipo/infer.rs index 8123d021..aaf20645 100644 --- a/crates/aiken-lang/src/tipo/infer.rs +++ b/crates/aiken-lang/src/tipo/infer.rs @@ -1,4 +1,4 @@ -use std::collections::HashMap; +use std::{collections::HashMap, ops::Deref, rc::Rc}; use crate::{ ast::{ @@ -11,10 +11,9 @@ use crate::{ builtins::function, expr::{TypedExpr, UntypedExpr}, line_numbers::LineNumbers, - tipo::{Span, Type}, + tipo::{Span, Type, TypeVar}, IdGenerator, }; -use std::rc::Rc; use super::{ environment::{generalise, EntityKind, Environment}, @@ -391,8 +390,16 @@ fn infer_definition( location: *location, }) } - Type::Fn { .. } | Type::Var { .. } => { - todo!("Fuzzer contains functions and/or non-concrete data-types?"); + Type::Var { tipo } => match tipo.borrow().deref() { + TypeVar::Link { tipo } => tipo_to_annotation(tipo, location), + _ => todo!( + "Fuzzer contains functions and/or non-concrete data-types? {tipo:#?}" + ), + }, + Type::Fn { .. } => { + todo!( + "Fuzzer contains functions and/or non-concrete data-types? {tipo:#?}" + ); } } } diff --git a/crates/aiken-project/src/lib.rs b/crates/aiken-project/src/lib.rs index 687d8f73..145fcc11 100644 --- a/crates/aiken-project/src/lib.rs +++ b/crates/aiken-project/src/lib.rs @@ -844,10 +844,12 @@ where let via = parameter.via.clone(); + let type_info = parameter.tipo.clone(); + let body = TypedExpr::Fn { location: Span::empty(), tipo: Rc::new(Type::Fn { - args: vec![parameter.tipo.clone()], + args: vec![type_info.clone()], ret: body.tipo(), }), is_capture: false, @@ -874,7 +876,7 @@ where name.to_string(), *can_error, program, - fuzzer, + (fuzzer, type_info), ); programs.push(prop); diff --git a/crates/aiken-project/src/script.rs b/crates/aiken-project/src/script.rs index 361f0853..386ab1b1 100644 --- a/crates/aiken-project/src/script.rs +++ b/crates/aiken-project/src/script.rs @@ -1,5 +1,6 @@ use crate::{pretty, ExBudget}; -use aiken_lang::ast::BinOp; +use aiken_lang::gen_uplc::builder::convert_data_to_type; +use aiken_lang::{ast::BinOp, tipo::Type}; use pallas::codec::utils::Int; use pallas::ledger::primitives::alonzo::{BigInt, Constr, PlutusData}; use std::{ @@ -10,7 +11,7 @@ use std::{ }; use uplc::{ ast::{Constant, Data, NamedDeBruijn, Program, Term}, - machine::{eval_result::EvalResult, value::from_pallas_bigint}, + machine::eval_result::EvalResult, }; // ---------------------------------------------------------------------------- @@ -66,7 +67,7 @@ impl Test { name: String, can_error: bool, program: Program, - fuzzer: Program, + fuzzer: (Program, Rc), ) -> Test { Test::PropertyTest(PropertyTest { input_path, @@ -123,7 +124,7 @@ pub struct PropertyTest { pub name: String, pub can_error: bool, pub program: Program, - pub fuzzer: Program, + pub fuzzer: (Program, Rc), } unsafe impl Send for PropertyTest {} @@ -170,7 +171,7 @@ impl PropertyTest { fn run_once(&self, seed: u32) -> (u32, Option>) { let (next_prng, value) = Prng::from_seed(seed) - .sample(&self.fuzzer) + .sample(&self.fuzzer.0, &self.fuzzer.1) .expect("running seeded Prng cannot fail."); let result = self.program.apply_term(&value).eval(ExBudget::max()); @@ -186,7 +187,7 @@ impl PropertyTest { choices: next_prng.choices(), can_error: self.can_error, program: &self.program, - fuzzer: &self.fuzzer, + fuzzer: (&self.fuzzer.0, &self.fuzzer.1), }; if !counterexample.choices.is_empty() { @@ -280,14 +281,18 @@ impl Prng { } /// Generate a pseudo-random value from a fuzzer using the given PRNG. - pub fn sample(&self, fuzzer: &Program) -> Option<(Prng, Term)> { + pub fn sample( + &self, + fuzzer: &Program, + return_type: &Type, + ) -> Option<(Prng, Term)> { let result = fuzzer .apply_data(self.uplc()) .eval(ExBudget::max()) .result() .expect("Fuzzer crashed?"); - Prng::from_result(result) + Prng::from_result(result, return_type) } /// Obtain a Prng back from a fuzzer execution. As a reminder, fuzzers have the following @@ -300,7 +305,10 @@ impl Prng { /// made during shrinking aren't breaking underlying invariants (if only, because we run out of /// values to replay). In such case, the replayed sequence is simply invalid and the fuzzer /// aborted altogether with 'None'. - pub fn from_result(result: Term) -> Option<(Self, Term)> { + pub fn from_result( + result: Term, + type_info: &Type, + ) -> Option<(Self, Term)> { /// Interpret the given 'PlutusData' as one of two Prng constructors. fn as_prng(cst: &PlutusData) -> Prng { if let PlutusData::Constr(Constr { tag, fields, .. }) = cst { @@ -315,10 +323,12 @@ impl Prng { } if *tag == 121 + Prng::REPLAYED { - return Prng::Replayed { - choices: fields.iter().map(as_u32).collect(), - uplc: cst.clone(), - }; + if let [PlutusData::Array(choices)] = &fields[..] { + return Prng::Replayed { + choices: choices.iter().map(as_u32).collect(), + uplc: cst.clone(), + }; + } } } @@ -333,25 +343,17 @@ impl Prng { panic!("Malformed choice's value: {field:#?}") } - /// Convert wrapped integer & bytearrays as raw constant terms. Because fuzzer - /// return a pair, those values end up being wrapped in 'Data', but test - /// functions will expect them in their raw constant form. - /// - /// Anything else is Data, so we're good. - fn as_value(data: &PlutusData) -> Term { - Term::Constant(Rc::new(match data { - PlutusData::BigInt(n) => Constant::Integer(from_pallas_bigint(n)), - PlutusData::BoundedBytes(bytes) => Constant::ByteString(bytes.clone().into()), - _ => Constant::Data(data.clone()), - })) - } - if let Term::Constant(rc) = &result { if let Constant::Data(PlutusData::Constr(Constr { tag, fields, .. })) = &rc.borrow() { if *tag == 121 + Prng::OK { if let [PlutusData::Array(elems)] = &fields[..] { if let [new_seed, value] = &elems[..] { - return Some((as_prng(new_seed), as_value(value))); + return Some(( + as_prng(new_seed), + convert_data_to_type(Term::data(value.clone()), type_info) + .try_into() + .expect("safe conversion from Name -> NamedDeBruijn"), + )); } } } @@ -388,7 +390,7 @@ pub struct Counterexample<'a> { pub result: EvalResult, pub can_error: bool, pub program: &'a Program, - pub fuzzer: &'a Program, + pub fuzzer: (&'a Program, &'a Type), } impl<'a> Counterexample<'a> { @@ -402,7 +404,7 @@ impl<'a> Counterexample<'a> { // test cases many times. Given that tests are fully deterministic, we can // memoize the already seen choices to avoid re-running the generators and // the test (which can be quite expensive). - match Prng::from_choices(choices).sample(self.fuzzer) { + match Prng::from_choices(choices).sample(self.fuzzer.0, self.fuzzer.1) { // Shrinked choices led to an impossible generation. None => false, @@ -455,32 +457,45 @@ impl<'a> Counterexample<'a> { loop { prev = self.choices.clone(); - // Delete choices by chunks of size 8, 4, 2, 1. - let mut k: isize = 8; + // First try deleting each choice we made in chunks. We try longer chunks because this + // allows us to delete whole composite elements: e.g. deleting an element from a + // generated list requires us to delete both the choice of whether to include it and + // also the element itself, which may involve more than one choice. + let mut k = 8; while k > 0 { - let mut i: isize = (self.choices.len() as isize) - k - 1; - while i >= 0 { - if i >= self.choices.len() as isize { - i -= 1; - continue; + if k > self.choices.len() { + break; + } + + for (i, j) in (0..=self.choices.len() - k).map(|i| (i, i + k)).rev() { + let mut choices = [ + &self.choices[..i], + if j < self.choices.len() { + &self.choices[j..] + } else { + &[] + }, + ] + .concat(); + + if self.consider(&choices) { + break; } - let mut choices = self.choices[0..(i + k) as usize].to_vec(); - if !self.consider(&choices) { - // Perform an extra reduction step that decrease the size of choices near - // the end, to cope with dependencies between choices, e.g. drawing a - // number as a list length, and then drawing that many elements. - // - // This isn't perfect, but allows to make progresses in many cases. - if i > 0 && *choices.get((i - 1) as usize).unwrap_or(&0) > 0 { - choices[(i - 1) as usize] -= 1; - if self.consider(&choices) { - i += 1; - } - } - i -= 1; + + // Perform an extra reduction step that decrease the size of choices near + // the end, to cope with dependencies between choices, e.g. drawing a + // number as a list length, and then drawing that many elements. + // + // This isn't perfect, but allows to make progresses in many cases. + if i > 0 && choices[i - 1] > 0 { + choices[i - 1] -= 1; + if self.consider(&choices) { + break; + }; } } - k /= 2; + + k /= 2 } // Now we try replacing region of choices with zeroes. Note that unlike the above we @@ -489,11 +504,9 @@ impl<'a> Counterexample<'a> { let mut k: isize = 8; while k > 1 { let mut i: isize = self.choices.len() as isize - k; - while i >= 0 { i -= if self.zeroes(i, k) { k } else { 1 } } - k /= 2 } diff --git a/crates/aiken-project/src/telemetry.rs b/crates/aiken-project/src/telemetry.rs index 3f61f1dd..812e9b94 100644 --- a/crates/aiken-project/src/telemetry.rs +++ b/crates/aiken-project/src/telemetry.rs @@ -304,23 +304,23 @@ fn fmt_test(result: &TestResult, max_mem: usize, max_cpu: usize, styled: bool) - ); // CounterExample - // if let TestResult::PropertyTestResult(PropertyTestResult { - // counterexample: Some(counterexample), - // .. - // }) = result - // { - // test = format!( - // "{test}\n{}", - // pretty::boxed_with( - // &pretty::style_if(styled, "counterexample".to_string(), |s| s - // .if_supports_color(Stderr, |s| s.red()) - // .if_supports_color(Stderr, |s| s.bold()) - // .to_string()), - // &counterexample.to_pretty(), - // |s| s.red().to_string() - // ) - // ) - // } + if let TestResult::PropertyTestResult(PropertyTestResult { + counterexample: Some(counterexample), + .. + }) = result + { + test = format!( + "{test}\n{}", + pretty::boxed_with( + &pretty::style_if(styled, "counterexample".to_string(), |s| s + .if_supports_color(Stderr, |s| s.red()) + .if_supports_color(Stderr, |s| s.bold()) + .to_string()), + &counterexample.to_pretty(), + |s| s.red().to_string() + ) + ) + } // Traces if !result.logs().is_empty() {