Implement reification from Maps.

This commit is contained in:
KtorZ 2024-03-01 16:14:23 +01:00
parent 5272f5ecee
commit bfcfc5c41b
No known key found for this signature in database
GPG Key ID: 33173CB6F77F4277
9 changed files with 257 additions and 184 deletions

View File

@ -1,4 +1,5 @@
use std::{collections::HashMap, rc::Rc}; use std::{collections::HashMap, rc::Rc};
use uplc::KeyValuePairs;
use vec1::Vec1; use vec1::Vec1;
@ -607,32 +608,38 @@ impl UntypedExpr {
preferred_format: ByteArrayFormatPreference::HexadecimalString, preferred_format: ByteArrayFormatPreference::HexadecimalString,
}), }),
PlutusData::Array(args) => { PlutusData::Array(args) => {
let inner;
match tipo { match tipo {
Type::App { Type::App {
module, name, args, .. module,
name,
args: type_args,
..
} if module.is_empty() && name.as_str() == "List" => { } if module.is_empty() && name.as_str() == "List" => {
if let [arg] = &args[..] { if let [inner] = &type_args[..] {
inner = arg.clone()
} 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:?}"
))
}
}
Ok(UntypedExpr::List { Ok(UntypedExpr::List {
location: Span::empty(), location: Span::empty(),
elements: args elements: args
.into_iter() .into_iter()
.map(|arg| UntypedExpr::reify(data_types, arg, &inner)) .map(|arg| UntypedExpr::reify(data_types, arg, inner))
.collect::<Result<Vec<_>, _>>()?, .collect::<Result<Vec<_>, _>>()?,
tail: None, tail: None,
}) })
} else {
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::<Result<Vec<_>, _>>()?,
}),
_ => Err(format!(
"invalid type annotation. expected List but got: {tipo:?}"
)),
}
} }
PlutusData::Constr(Constr { 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,
)
}
} }
} }

View File

@ -75,6 +75,10 @@ pub struct CodeGenerator<'a> {
} }
impl<'a> CodeGenerator<'a> { impl<'a> CodeGenerator<'a> {
pub fn data_types(&self) -> &IndexMap<DataTypeKey, &'a TypedDataType> {
&self.data_types
}
pub fn new( pub fn new(
functions: IndexMap<FunctionAccessKey, &'a TypedFunction>, functions: IndexMap<FunctionAccessKey, &'a TypedFunction>,
data_types: IndexMap<DataTypeKey, &'a TypedDataType>, data_types: IndexMap<DataTypeKey, &'a TypedDataType>,

View File

@ -57,6 +57,18 @@ pub enum Type {
} }
impl 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 { pub fn is_result_constructor(&self) -> bool {
match self { match self {
Type::Fn { ret, .. } => ret.is_result(), Type::Fn { ret, .. } => ret.is_result(),

View File

@ -34,6 +34,7 @@ use aiken_lang::{
}, },
builtins, builtins,
expr::{TypedExpr, UntypedExpr}, expr::{TypedExpr, UntypedExpr},
gen_uplc::builder::convert_opaque_type,
tipo::{Type, TypeInfo}, tipo::{Type, TypeInfo},
IdGenerator, IdGenerator,
}; };
@ -46,7 +47,7 @@ use pallas::ledger::{
primitives::babbage::{self as cardano, PolicyId}, primitives::babbage::{self as cardano, PolicyId},
traverse::ComputeHash, traverse::ComputeHash,
}; };
use script::{Assertion, Test, TestResult}; use script::{Assertion, Fuzzer, Test, TestResult};
use std::{ use std::{
collections::HashMap, collections::HashMap,
fs::{self, File}, fs::{self, File},
@ -844,7 +845,7 @@ where
let via = parameter.via.clone(); let via = parameter.via.clone();
let type_info = parameter.tipo.clone(); let type_info = convert_opaque_type(&parameter.tipo, generator.data_types());
let body = TypedExpr::Fn { let body = TypedExpr::Fn {
location: Span::empty(), location: Span::empty(),
@ -876,7 +877,10 @@ where
name.to_string(), name.to_string(),
*can_error, *can_error,
program, program,
(fuzzer, type_info), Fuzzer {
program: fuzzer,
type_info,
},
); );
programs.push(prop); programs.push(prop);

View File

@ -73,7 +73,7 @@ impl Test {
name: String, name: String,
can_error: bool, can_error: bool,
program: Program<NamedDeBruijn>, program: Program<NamedDeBruijn>,
fuzzer: (Program<NamedDeBruijn>, Rc<Type>), fuzzer: Fuzzer<NamedDeBruijn>,
) -> Test { ) -> Test {
Test::PropertyTest(PropertyTest { Test::PropertyTest(PropertyTest {
input_path, input_path,
@ -130,11 +130,17 @@ pub struct PropertyTest {
pub name: String, pub name: String,
pub can_error: bool, pub can_error: bool,
pub program: Program<NamedDeBruijn>, pub program: Program<NamedDeBruijn>,
pub fuzzer: (Program<NamedDeBruijn>, Rc<Type>), pub fuzzer: Fuzzer<NamedDeBruijn>,
} }
unsafe impl Send for PropertyTest {} unsafe impl Send for PropertyTest {}
#[derive(Debug, Clone)]
pub struct Fuzzer<T> {
pub program: Program<T>,
pub type_info: Rc<Type>,
}
impl PropertyTest { impl PropertyTest {
const MAX_TEST_RUN: usize = 100; const MAX_TEST_RUN: usize = 100;
@ -177,7 +183,7 @@ impl PropertyTest {
fn run_once(&self, seed: u32) -> (u32, Option<PlutusData>) { fn run_once(&self, seed: u32) -> (u32, Option<PlutusData>) {
let (next_prng, value) = Prng::from_seed(seed) let (next_prng, value) = Prng::from_seed(seed)
.sample(&self.fuzzer.0) .sample(&self.fuzzer.program)
.expect("running seeded Prng cannot fail."); .expect("running seeded Prng cannot fail.");
let result = self.eval(&value); let result = self.eval(&value);
@ -207,7 +213,7 @@ impl PropertyTest {
} }
pub fn eval(&self, value: &PlutusData) -> EvalResult { 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() .try_into()
.expect("safe conversion from Name -> NamedDeBruijn"); .expect("safe conversion from Name -> NamedDeBruijn");
self.program.apply_term(&term).eval(ExBudget::max()) 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 // test cases many times. Given that tests are fully deterministic, we can
// memoize the already seen choices to avoid re-running the generators and // memoize the already seen choices to avoid re-running the generators and
// the test (which can be quite expensive). // 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. // Shrinked choices led to an impossible generation.
None => false, None => false,
@ -653,7 +659,7 @@ impl PropertyTestResult<PlutusData> {
counterexample: match self.counterexample { counterexample: match self.counterexample {
None => None, None => None,
Some(counterexample) => Some( 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?"), .expect("Failed to reify counterexample?"),
), ),
}, },

View File

@ -1,7 +1,16 @@
# This file was generated by Aiken # This file was generated by Aiken
# You typically do not need to edit this file # 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 = [] requirements = []
packages = [] source = "github"
[etags] [etags]
"aiken-lang/stdlib@main" = [{ secs_since_epoch = 1709304545, nanos_since_epoch = 842241000 }, "cf946239d3dd481ed41f20e56bf24910b5229ea35aa171a708edc2a47fc20a7b"]

View File

@ -1,3 +1,8 @@
name = "aiken-lang/acceptance_test_095" name = "aiken-lang/acceptance_test_095"
version = "0.0.0" version = "0.0.0"
description = "" description = ""
[[dependencies]]
name = "aiken-lang/stdlib"
version = "main"
source = "github"

View File

@ -0,0 +1,130 @@
use aiken/builtin
const max_int: Int = 255
pub type PRNG {
Seeded { seed: Int, choices: List<Int> }
Replayed { choices: List<Int> }
}
pub type Fuzzer<a> =
fn(PRNG) -> Option<(PRNG, a)>
// Primitives
pub fn any_int() -> Fuzzer<Int> {
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<a> {
fn(s0) { Some((s0, a)) }
}
pub fn and_then(fuzz_a: Fuzzer<a>, f: fn(a) -> Fuzzer<b>) -> Fuzzer<b> {
fn(s0) {
when fuzz_a(s0) is {
Some((s1, a)) -> f(a)(s1)
None -> None
}
}
}
pub fn map(fuzz_a: Fuzzer<a>, f: fn(a) -> b) -> Fuzzer<b> {
fn(s0) {
when fuzz_a(s0) is {
Some((s1, a)) -> Some((s1, f(a)))
None -> None
}
}
}
pub fn map2(fuzz_a: Fuzzer<a>, fuzz_b: Fuzzer<b>, f: fn(a, b) -> c) -> Fuzzer<c> {
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<a>,
fuzz_b: Fuzzer<b>,
fuzz_c: Fuzzer<c>,
fuzz_d: Fuzzer<d>,
f: fn(a, b, c, d) -> result,
) -> Fuzzer<result> {
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<Bool> {
any_int() |> map(fn(n) { n <= 127 })
}
fn any_list(fuzz_a: Fuzzer<a>) -> Fuzzer<List<a>> {
any_bool()
|> and_then(
fn(continue) {
if continue {
map2(fuzz_a, any_list(fuzz_a), fn(head, tail) { [head, ..tail] })
} else {
constant([])
}
},
)
}

View File

@ -1,166 +1,47 @@
use aiken/builtin use aiken/dict.{Dict}
use aiken/fuzz.{Fuzzer}
const max_int: Int = 255 use aiken/int
pub type PRNG {
Seeded { seed: Int, choices: List<Int> }
Replayed { choices: List<Int> }
}
type Fuzzer<a> =
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<a> {
fn(s0) { Some((s0, a)) }
}
pub fn and_then(fuzz_a: Fuzzer<a>, f: fn(a) -> Fuzzer<b>) -> Fuzzer<b> {
fn(s0) {
when fuzz_a(s0) is {
Some((s1, a)) -> f(a)(s1)
None -> None
}
}
}
pub fn map(fuzz_a: Fuzzer<a>, f: fn(a) -> b) -> Fuzzer<b> {
fn(s0) {
when fuzz_a(s0) is {
Some((s1, a)) -> Some((s1, f(a)))
None -> None
}
}
}
pub fn map2(fuzz_a: Fuzzer<a>, fuzz_b: Fuzzer<b>, f: fn(a, b) -> c) -> Fuzzer<c> {
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<Bool> {
any_int |> map(fn(n) { n <= 127 })
}
fn any_list(fuzz_a: Fuzzer<a>) -> Fuzzer<List<a>> {
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<Season> {
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<a>) -> Int {
when xs is {
[] -> 0
[_, ..tail] -> 1 + length(tail)
}
}
fn filter(xs: List<a>, f: fn(a) -> Bool) -> List<a> {
when xs is {
[] ->
[]
[head, ..tail] ->
if f(head) {
[head, ..filter(tail, f)]
} else {
filter(tail, f)
}
}
}
// Properties
pub type Season { pub type Season {
Winter(Bool) Winter
Spring(Int) Spring
Summer Summer
Fall Fall
} }
// test prop_is_never_summer(xs via any_list(any_season())) { fn compare_season(a: Season, b: Season) -> Ordering {
// filter(xs, fn(x) { x == Summer }) == [] 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())) { int.compare(season_to_int(a), season_to_int(b))
is_always_cold_in_winter(xs)
} }
test prop_is_always_cold_in_winter_2() { fn any_year() -> Fuzzer<Dict<Season, Int>> {
is_always_cold_in_winter([Winter(True)]) 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<Season>) -> Bool { test prop_always_cold_in_winter(year via any_year()) {
when xs is { when dict.get(year, Winter) is {
[Winter(cold), ..tail] -> cold && is_always_cold_in_winter(tail) Some(temperature) -> temperature <= 10
_ -> True _ -> fail @"failed to get?"
} }
} }