diff --git a/crates/aiken-lang/src/expr.rs b/crates/aiken-lang/src/expr.rs index 976c47df..1fdea112 100644 --- a/crates/aiken-lang/src/expr.rs +++ b/crates/aiken-lang/src/expr.rs @@ -1,4 +1,5 @@ use std::{collections::HashMap, rc::Rc}; +use uplc::KeyValuePairs; use vec1::Vec1; @@ -607,32 +608,38 @@ impl UntypedExpr { preferred_format: ByteArrayFormatPreference::HexadecimalString, }), PlutusData::Array(args) => { - let inner; match tipo { Type::App { - module, name, args, .. + module, + name, + args: type_args, + .. } if module.is_empty() && name.as_str() == "List" => { - if let [arg] = &args[..] { - inner = arg.clone() + if let [inner] = &type_args[..] { + Ok(UntypedExpr::List { + location: Span::empty(), + elements: args + .into_iter() + .map(|arg| UntypedExpr::reify(data_types, arg, inner)) + .collect::, _>>()?, + tail: None, + }) } else { - return Err("invalid List type annotation: the list has multiple type-parameters.".to_string()); - }; - } - _ => { - return Err(format!( - "invalid type annotation. expected List but got: {tipo:?}" - )) + Err("invalid List type annotation: the list has multiple type-parameters.".to_string()) + } } + Type::Tuple { elems } => Ok(UntypedExpr::Tuple { + location: Span::empty(), + elems: args + .into_iter() + .zip(elems) + .map(|(arg, arg_type)| UntypedExpr::reify(data_types, arg, arg_type)) + .collect::, _>>()?, + }), + _ => Err(format!( + "invalid type annotation. expected List but got: {tipo:?}" + )), } - - Ok(UntypedExpr::List { - location: Span::empty(), - elements: args - .into_iter() - .map(|arg| UntypedExpr::reify(data_types, arg, &inner)) - .collect::, _>>()?, - tail: None, - }) } PlutusData::Constr(Constr { @@ -716,7 +723,22 @@ impl UntypedExpr { )) } - PlutusData::Map(..) => todo!("reify Map"), + PlutusData::Map(indef_or_def) => { + let kvs = match indef_or_def { + KeyValuePairs::Def(kvs) => kvs, + KeyValuePairs::Indef(kvs) => kvs, + }; + + UntypedExpr::reify( + data_types, + PlutusData::Array( + kvs.into_iter() + .map(|(k, v)| PlutusData::Array(vec![k, v])) + .collect(), + ), + tipo, + ) + } } } diff --git a/crates/aiken-lang/src/gen_uplc.rs b/crates/aiken-lang/src/gen_uplc.rs index 59182f88..c1eb90c5 100644 --- a/crates/aiken-lang/src/gen_uplc.rs +++ b/crates/aiken-lang/src/gen_uplc.rs @@ -75,6 +75,10 @@ pub struct CodeGenerator<'a> { } impl<'a> CodeGenerator<'a> { + pub fn data_types(&self) -> &IndexMap { + &self.data_types + } + pub fn new( functions: IndexMap, data_types: IndexMap, diff --git a/crates/aiken-lang/src/tipo.rs b/crates/aiken-lang/src/tipo.rs index 0f180ea5..c1ee9711 100644 --- a/crates/aiken-lang/src/tipo.rs +++ b/crates/aiken-lang/src/tipo.rs @@ -57,6 +57,18 @@ pub enum Type { } impl Type { + pub fn qualifier(&self) -> Option<(String, String)> { + match self { + Type::App { module, name, .. } => Some((module.to_string(), name.to_string())), + Type::Fn { .. } => None, + Type::Var { ref tipo } => match &*tipo.borrow() { + TypeVar::Link { ref tipo } => tipo.qualifier(), + _ => None, + }, + Type::Tuple { .. } => Some((String::new(), "Tuple".to_string())), + } + } + pub fn is_result_constructor(&self) -> bool { match self { Type::Fn { ret, .. } => ret.is_result(), diff --git a/crates/aiken-project/src/lib.rs b/crates/aiken-project/src/lib.rs index 349ff7d6..8849a091 100644 --- a/crates/aiken-project/src/lib.rs +++ b/crates/aiken-project/src/lib.rs @@ -34,6 +34,7 @@ use aiken_lang::{ }, builtins, expr::{TypedExpr, UntypedExpr}, + gen_uplc::builder::convert_opaque_type, tipo::{Type, TypeInfo}, IdGenerator, }; @@ -46,7 +47,7 @@ use pallas::ledger::{ primitives::babbage::{self as cardano, PolicyId}, traverse::ComputeHash, }; -use script::{Assertion, Test, TestResult}; +use script::{Assertion, Fuzzer, Test, TestResult}; use std::{ collections::HashMap, fs::{self, File}, @@ -844,7 +845,7 @@ where let via = parameter.via.clone(); - let type_info = parameter.tipo.clone(); + let type_info = convert_opaque_type(¶meter.tipo, generator.data_types()); let body = TypedExpr::Fn { location: Span::empty(), @@ -876,7 +877,10 @@ where name.to_string(), *can_error, program, - (fuzzer, type_info), + Fuzzer { + program: fuzzer, + type_info, + }, ); programs.push(prop); diff --git a/crates/aiken-project/src/script.rs b/crates/aiken-project/src/script.rs index 256929a3..ff297682 100644 --- a/crates/aiken-project/src/script.rs +++ b/crates/aiken-project/src/script.rs @@ -73,7 +73,7 @@ impl Test { name: String, can_error: bool, program: Program, - fuzzer: (Program, Rc), + fuzzer: Fuzzer, ) -> Test { Test::PropertyTest(PropertyTest { input_path, @@ -130,11 +130,17 @@ pub struct PropertyTest { pub name: String, pub can_error: bool, pub program: Program, - pub fuzzer: (Program, Rc), + pub fuzzer: Fuzzer, } unsafe impl Send for PropertyTest {} +#[derive(Debug, Clone)] +pub struct Fuzzer { + pub program: Program, + pub type_info: Rc, +} + impl PropertyTest { const MAX_TEST_RUN: usize = 100; @@ -177,7 +183,7 @@ impl PropertyTest { fn run_once(&self, seed: u32) -> (u32, Option) { let (next_prng, value) = Prng::from_seed(seed) - .sample(&self.fuzzer.0) + .sample(&self.fuzzer.program) .expect("running seeded Prng cannot fail."); let result = self.eval(&value); @@ -207,7 +213,7 @@ impl PropertyTest { } pub fn eval(&self, value: &PlutusData) -> EvalResult { - let term = convert_data_to_type(Term::data(value.clone()), &self.fuzzer.1) + let term = convert_data_to_type(Term::data(value.clone()), &self.fuzzer.type_info) .try_into() .expect("safe conversion from Name -> NamedDeBruijn"); self.program.apply_term(&term).eval(ExBudget::max()) @@ -399,7 +405,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.property.fuzzer.0) { + match Prng::from_choices(choices).sample(&self.property.fuzzer.program) { // Shrinked choices led to an impossible generation. None => false, @@ -653,7 +659,7 @@ impl PropertyTestResult { counterexample: match self.counterexample { None => None, Some(counterexample) => Some( - UntypedExpr::reify(data_types, counterexample, &self.test.fuzzer.1) + UntypedExpr::reify(data_types, counterexample, &self.test.fuzzer.type_info) .expect("Failed to reify counterexample?"), ), }, diff --git a/examples/acceptance_tests/095/aiken.lock b/examples/acceptance_tests/095/aiken.lock index 6e350cda..17a9c623 100644 --- a/examples/acceptance_tests/095/aiken.lock +++ b/examples/acceptance_tests/095/aiken.lock @@ -1,7 +1,16 @@ # This file was generated by Aiken # You typically do not need to edit this file +[[requirements]] +name = "aiken-lang/stdlib" +version = "main" +source = "github" + +[[packages]] +name = "aiken-lang/stdlib" +version = "main" requirements = [] -packages = [] +source = "github" [etags] +"aiken-lang/stdlib@main" = [{ secs_since_epoch = 1709304545, nanos_since_epoch = 842241000 }, "cf946239d3dd481ed41f20e56bf24910b5229ea35aa171a708edc2a47fc20a7b"] diff --git a/examples/acceptance_tests/095/aiken.toml b/examples/acceptance_tests/095/aiken.toml index 92735c12..0020ed78 100644 --- a/examples/acceptance_tests/095/aiken.toml +++ b/examples/acceptance_tests/095/aiken.toml @@ -1,3 +1,8 @@ name = "aiken-lang/acceptance_test_095" version = "0.0.0" description = "" + +[[dependencies]] +name = "aiken-lang/stdlib" +version = "main" +source = "github" diff --git a/examples/acceptance_tests/095/lib/aiken/fuzz.ak b/examples/acceptance_tests/095/lib/aiken/fuzz.ak new file mode 100644 index 00000000..be0f647a --- /dev/null +++ b/examples/acceptance_tests/095/lib/aiken/fuzz.ak @@ -0,0 +1,130 @@ +use aiken/builtin + +const max_int: Int = 255 + +pub type PRNG { + Seeded { seed: Int, choices: List } + Replayed { choices: List } +} + +pub type Fuzzer = + fn(PRNG) -> Option<(PRNG, a)> + +// Primitives + +pub fn any_int() -> Fuzzer { + fn(prng: PRNG) -> Option<(PRNG, Int)> { + when prng is { + Seeded { seed, choices } -> { + let digest = + seed + |> builtin.integer_to_bytearray(True, 32, _) + |> builtin.blake2b_256() + + let choice = + digest + |> builtin.index_bytearray(0) + + let new_seed = + digest + |> builtin.slice_bytearray(1, 4, _) + |> builtin.bytearray_to_integer(True, _) + + Some((Seeded { seed: new_seed, choices: [choice, ..choices] }, choice)) + } + + Replayed { choices } -> + when choices is { + [] -> None + [head, ..tail] -> + if head >= 0 && head <= max_int { + Some((Replayed { choices: tail }, head)) + } else { + None + } + } + } + } +} + +pub fn constant(a: a) -> Fuzzer { + fn(s0) { Some((s0, a)) } +} + +pub fn and_then(fuzz_a: Fuzzer, f: fn(a) -> Fuzzer) -> Fuzzer { + fn(s0) { + when fuzz_a(s0) is { + Some((s1, a)) -> f(a)(s1) + None -> None + } + } +} + +pub fn map(fuzz_a: Fuzzer, f: fn(a) -> b) -> Fuzzer { + fn(s0) { + when fuzz_a(s0) is { + Some((s1, a)) -> Some((s1, f(a))) + None -> None + } + } +} + +pub fn map2(fuzz_a: Fuzzer, fuzz_b: Fuzzer, f: fn(a, b) -> c) -> Fuzzer { + fn(s0) { + when fuzz_a(s0) is { + Some((s1, a)) -> + when fuzz_b(s1) is { + Some((s2, b)) -> Some((s2, f(a, b))) + None -> None + } + None -> None + } + } +} + +pub fn map4( + fuzz_a: Fuzzer, + fuzz_b: Fuzzer, + fuzz_c: Fuzzer, + fuzz_d: Fuzzer, + f: fn(a, b, c, d) -> result, +) -> Fuzzer { + fn(s0) { + when fuzz_a(s0) is { + Some((s1, a)) -> + when fuzz_b(s1) is { + Some((s2, b)) -> + when fuzz_c(s2) is { + Some((s3, c)) -> + when fuzz_d(s3) is { + Some((s4, d)) -> Some((s4, f(a, b, c, d))) + None -> None + } + None -> None + } + None -> None + } + + None -> None + } + } +} + +// Builders + +fn any_bool() -> Fuzzer { + any_int() |> map(fn(n) { n <= 127 }) +} + +fn any_list(fuzz_a: Fuzzer) -> Fuzzer> { + any_bool() + |> and_then( + fn(continue) { + if continue { + map2(fuzz_a, any_list(fuzz_a), fn(head, tail) { [head, ..tail] }) + } else { + constant([]) + } + }, + ) +} diff --git a/examples/acceptance_tests/095/lib/foo.ak b/examples/acceptance_tests/095/lib/foo.ak index 66a2b591..b73ea8d8 100644 --- a/examples/acceptance_tests/095/lib/foo.ak +++ b/examples/acceptance_tests/095/lib/foo.ak @@ -1,166 +1,47 @@ -use aiken/builtin - -const max_int: Int = 255 - -pub type PRNG { - Seeded { seed: Int, choices: List } - Replayed { choices: List } -} - -type Fuzzer = - fn(PRNG) -> Option<(PRNG, a)> - -// Primitives - -fn any_int(prng: PRNG) -> Option<(PRNG, Int)> { - when prng is { - Seeded { seed, choices } -> { - let digest = - seed - |> builtin.integer_to_bytearray(True, 32, _) - |> builtin.blake2b_256() - - let choice = - digest - |> builtin.index_bytearray(0) - - let new_seed = - digest - |> builtin.slice_bytearray(1, 4, _) - |> builtin.bytearray_to_integer(True, _) - - Some((Seeded { seed: new_seed, choices: [choice, ..choices] }, choice)) - } - - Replayed { choices } -> - when choices is { - [] -> None - [head, ..tail] -> - if head >= 0 && head <= max_int { - Some((Replayed { choices: tail }, head)) - } else { - None - } - } - } -} - -pub fn constant(a: a) -> Fuzzer { - fn(s0) { Some((s0, a)) } -} - -pub fn and_then(fuzz_a: Fuzzer, f: fn(a) -> Fuzzer) -> Fuzzer { - fn(s0) { - when fuzz_a(s0) is { - Some((s1, a)) -> f(a)(s1) - None -> None - } - } -} - -pub fn map(fuzz_a: Fuzzer, f: fn(a) -> b) -> Fuzzer { - fn(s0) { - when fuzz_a(s0) is { - Some((s1, a)) -> Some((s1, f(a))) - None -> None - } - } -} - -pub fn map2(fuzz_a: Fuzzer, fuzz_b: Fuzzer, f: fn(a, b) -> c) -> Fuzzer { - fn(s0) { - when fuzz_a(s0) is { - Some((s1, a)) -> - when fuzz_b(s1) is { - Some((s2, b)) -> Some((s2, f(a, b))) - None -> None - } - None -> None - } - } -} - -// Builders - -fn any_bool() -> Fuzzer { - any_int |> map(fn(n) { n <= 127 }) -} - -fn any_list(fuzz_a: Fuzzer) -> Fuzzer> { - any_bool() - |> and_then( - fn(continue) { - if continue { - map2(fuzz_a, any_list(fuzz_a), fn(head, tail) { [head, ..tail] }) - } else { - constant([]) - } - }, - ) -} - -fn any_season() -> Fuzzer { - any_int - |> and_then( - fn(i) { - let n = i % 3 - if n == 0 { - any_bool() |> map(Winter) - } else if n == 1 { - constant(Spring(i)) - } else if n == 2 { - constant(Summer) - } else { - constant(Fall) - } - }, - ) -} - -fn length(xs: List) -> Int { - when xs is { - [] -> 0 - [_, ..tail] -> 1 + length(tail) - } -} - -fn filter(xs: List, f: fn(a) -> Bool) -> List { - when xs is { - [] -> - [] - [head, ..tail] -> - if f(head) { - [head, ..filter(tail, f)] - } else { - filter(tail, f) - } - } -} - -// Properties +use aiken/dict.{Dict} +use aiken/fuzz.{Fuzzer} +use aiken/int pub type Season { - Winter(Bool) - Spring(Int) + Winter + Spring Summer Fall } -// test prop_is_never_summer(xs via any_list(any_season())) { -// filter(xs, fn(x) { x == Summer }) == [] -// } +fn compare_season(a: Season, b: Season) -> Ordering { + let season_to_int = + fn(season) { + when season is { + Winter -> 0 + Spring -> 1 + Summer -> 2 + Fall -> 3 + } + } -test prop_is_always_cold_in_winter(xs via any_list(any_season())) { - is_always_cold_in_winter(xs) + int.compare(season_to_int(a), season_to_int(b)) } -test prop_is_always_cold_in_winter_2() { - is_always_cold_in_winter([Winter(True)]) +fn any_year() -> Fuzzer> { + fuzz.map4( + fuzz.any_int(), + fuzz.any_int(), + fuzz.any_int(), + fuzz.any_int(), + fn(a, b, c, d) { + dict.new() + |> dict.insert(Winter, a, compare_season) + |> dict.insert(Spring, b, compare_season) + |> dict.insert(Summer, c, compare_season) + |> dict.insert(Fall, d, compare_season) + }, + ) } -fn is_always_cold_in_winter(xs: List) -> Bool { - when xs is { - [Winter(cold), ..tail] -> cold && is_always_cold_in_winter(tail) - _ -> True +test prop_always_cold_in_winter(year via any_year()) { + when dict.get(year, Winter) is { + Some(temperature) -> temperature <= 10 + _ -> fail @"failed to get?" } }