Rework generate_raw to avoid need to intern in prop tests

Also, this commit makes `apply_term` automatically re-intern the
  program since it isn't safe to apply any term onto a UPLC program. In
  particular, terms that introduce new let-bindings (via lambdas) will
  mess with the already generated DeBruijn indices.

  The problem doesn't occur for pure constant terms like Data. So we
  still have a safe and fast version 'apply_data' when needed.
This commit is contained in:
KtorZ 2024-03-03 19:24:35 +01:00
parent 1134b8d7d0
commit 30841fe000
No known key found for this signature in database
GPG Key ID: 33173CB6F77F4277
9 changed files with 127 additions and 120 deletions

View File

@ -184,8 +184,13 @@ impl<'a> CodeGenerator<'a> {
self.finalize(term) self.finalize(term)
} }
pub fn generate_raw(&mut self, test_body: &TypedExpr, module_name: &str) -> Program<Name> { pub fn generate_raw(
let mut air_tree = self.build(test_body, module_name, &[]); &mut self,
body: &TypedExpr,
args: &[TypedArg],
module_name: &str,
) -> Program<Name> {
let mut air_tree = self.build(body, module_name, &[]);
air_tree = AirTree::no_op(air_tree); air_tree = AirTree::no_op(air_tree);
@ -194,7 +199,13 @@ impl<'a> CodeGenerator<'a> {
// optimizations on air tree // optimizations on air tree
let full_vec = full_tree.to_vec(); let full_vec = full_tree.to_vec();
let term = self.uplc_code_gen(full_vec); let mut term = self.uplc_code_gen(full_vec);
term = if args.is_empty() {
term
} else {
cast_validator_args(term, args)
};
self.finalize(term) self.finalize(term)
} }

View File

@ -5,7 +5,7 @@ use super::{
}; };
use std::{iter, ops::Deref}; use std::{iter, ops::Deref};
use uplc::{ use uplc::{
ast::{Constant, Data as UplcData, DeBruijn, Term}, ast::{Constant, Data as UplcData},
PlutusData, PlutusData,
}; };
@ -30,7 +30,7 @@ impl Parameter {
pub fn validate( pub fn validate(
&self, &self,
definitions: &Definitions<Annotated<Schema>>, definitions: &Definitions<Annotated<Schema>>,
term: &Term<DeBruijn>, constant: &Constant,
) -> Result<(), Error> { ) -> Result<(), Error> {
let schema = &definitions let schema = &definitions
.lookup(&self.schema) .lookup(&self.schema)
@ -42,11 +42,7 @@ impl Parameter {
})? })?
.annotated; .annotated;
if let Term::Constant(constant) = term {
validate_schema(schema, definitions, constant) validate_schema(schema, definitions, constant)
} else {
Err(Error::NonConstantParameter)
}
} }
} }
@ -352,11 +348,13 @@ fn expect_data_constr(term: &Constant, index: usize) -> Result<Vec<Constant>, Er
Err(mismatch( Err(mismatch(
term, term,
Schema::Data(Data::AnyOf(vec![Constructor { Schema::Data(Data::AnyOf(vec![
Constructor {
index, index,
fields: vec![], fields: vec![],
} }
.into()])), .into(),
])),
)) ))
} }

View File

@ -12,9 +12,9 @@ use aiken_lang::{
}; };
use miette::NamedSource; use miette::NamedSource;
use serde; use serde;
use std::{borrow::Borrow, rc::Rc}; use std::borrow::Borrow;
use uplc::{ use uplc::{
ast::{Constant, DeBruijn, Program, Term}, ast::{Constant, DeBruijn, Program},
PlutusData, PlutusData,
}; };
@ -198,14 +198,14 @@ impl Validator {
pub fn apply( pub fn apply(
self, self,
definitions: &Definitions<Annotated<Schema>>, definitions: &Definitions<Annotated<Schema>>,
arg: &Term<DeBruijn>, arg: &PlutusData,
) -> Result<Self, Error> { ) -> Result<Self, Error> {
match self.parameters.split_first() { match self.parameters.split_first() {
None => Err(Error::NoParametersToApply), None => Err(Error::NoParametersToApply),
Some((head, tail)) => { Some((head, tail)) => {
head.validate(definitions, arg)?; head.validate(definitions, &Constant::Data(arg.clone()))?;
Ok(Self { Ok(Self {
program: self.program.apply_term(arg), program: self.program.apply_data(arg.clone()),
parameters: tail.to_vec(), parameters: tail.to_vec(),
..self ..self
}) })
@ -217,7 +217,7 @@ impl Validator {
&self, &self,
definitions: &Definitions<Annotated<Schema>>, definitions: &Definitions<Annotated<Schema>>,
ask: F, ask: F,
) -> Result<Term<DeBruijn>, Error> ) -> Result<PlutusData, Error>
where where
F: Fn(&Annotated<Schema>, &Definitions<Annotated<Schema>>) -> Result<PlutusData, Error>, F: Fn(&Annotated<Schema>, &Definitions<Annotated<Schema>>) -> Result<PlutusData, Error>,
{ {
@ -241,7 +241,7 @@ impl Validator {
let data = ask(&schema, definitions)?; let data = ask(&schema, definitions)?;
Ok(Term::Constant(Rc::new(Constant::Data(data.clone())))) Ok(data.clone())
} }
} }
} }
@ -619,7 +619,7 @@ mod tests {
fn validate_arguments_integer() { fn validate_arguments_integer() {
let definitions = fixture_definitions(); let definitions = fixture_definitions();
let term = Term::data(uplc_ast::Data::integer(42.into())); let term = Constant::Data(uplc_ast::Data::integer(42.into()));
let param = Parameter { let param = Parameter {
title: None, title: None,
@ -633,7 +633,7 @@ mod tests {
fn validate_arguments_bytestring() { fn validate_arguments_bytestring() {
let definitions = fixture_definitions(); let definitions = fixture_definitions();
let term = Term::data(uplc_ast::Data::bytestring(vec![102, 111, 111])); let term = Constant::Data(uplc_ast::Data::bytestring(vec![102, 111, 111]));
let param = Parameter { let param = Parameter {
title: None, title: None,
@ -662,7 +662,7 @@ mod tests {
.into(), .into(),
); );
let term = Term::data(uplc_ast::Data::list(vec![ let term = Constant::Data(uplc_ast::Data::list(vec![
uplc_ast::Data::integer(42.into()), uplc_ast::Data::integer(42.into()),
uplc_ast::Data::integer(14.into()), uplc_ast::Data::integer(14.into()),
])); ]));
@ -691,7 +691,7 @@ mod tests {
.into(), .into(),
); );
let term = Term::data(uplc_ast::Data::list(vec![uplc_ast::Data::bytestring( let term = Constant::Data(uplc_ast::Data::list(vec![uplc_ast::Data::bytestring(
vec![102, 111, 111], vec![102, 111, 111],
)])); )]));
@ -723,7 +723,7 @@ mod tests {
.into(), .into(),
); );
let term = Term::data(uplc_ast::Data::list(vec![ let term = Constant::Data(uplc_ast::Data::list(vec![
uplc_ast::Data::integer(42.into()), uplc_ast::Data::integer(42.into()),
uplc_ast::Data::bytestring(vec![102, 111, 111]), uplc_ast::Data::bytestring(vec![102, 111, 111]),
])); ]));
@ -754,7 +754,7 @@ mod tests {
.into(), .into(),
); );
let term = Term::data(uplc_ast::Data::map(vec![( let term = Constant::Data(uplc_ast::Data::map(vec![(
uplc_ast::Data::bytestring(vec![102, 111, 111]), uplc_ast::Data::bytestring(vec![102, 111, 111]),
uplc_ast::Data::integer(42.into()), uplc_ast::Data::integer(42.into()),
)])); )]));
@ -770,7 +770,7 @@ mod tests {
let definitions = fixture_definitions(); let definitions = fixture_definitions();
let term = Term::data(uplc_ast::Data::constr(1, vec![])); let term = Constant::Data(uplc_ast::Data::constr(1, vec![]));
let param: Parameter = schema.into(); let param: Parameter = schema.into();
@ -807,7 +807,7 @@ mod tests {
.into(), .into(),
); );
let term = Term::data(uplc_ast::Data::constr( let term = Constant::Data(uplc_ast::Data::constr(
0, 0,
vec![uplc_ast::Data::constr(0, vec![])], vec![uplc_ast::Data::constr(0, vec![])],
)); ));
@ -863,7 +863,7 @@ mod tests {
.into(), .into(),
); );
let term = Term::data(uplc_ast::Data::constr( let term = Constant::Data(uplc_ast::Data::constr(
1, 1,
vec![ vec![
uplc_ast::Data::integer(14.into()), uplc_ast::Data::integer(14.into()),

View File

@ -59,7 +59,7 @@ use std::{
use telemetry::EventListener; use telemetry::EventListener;
use test_framework::{Test, TestResult}; use test_framework::{Test, TestResult};
use uplc::{ use uplc::{
ast::{DeBruijn, Name, Program, Term}, ast::{Name, Program},
PlutusData, PlutusData,
}; };
@ -445,7 +445,7 @@ where
&self, &self,
title: Option<&String>, title: Option<&String>,
ask: F, ask: F,
) -> Result<Term<DeBruijn>, Error> ) -> Result<PlutusData, Error>
where where
F: Fn( F: Fn(
&Annotated<Schema>, &Annotated<Schema>,
@ -462,19 +462,19 @@ where
|known_validators| Error::MoreThanOneValidatorFound { known_validators }; |known_validators| Error::MoreThanOneValidatorFound { known_validators };
let when_missing = |known_validators| Error::NoValidatorNotFound { known_validators }; let when_missing = |known_validators| Error::NoValidatorNotFound { known_validators };
let term = blueprint.with_validator(title, when_too_many, when_missing, |validator| { let data = blueprint.with_validator(title, when_too_many, when_missing, |validator| {
validator validator
.ask_next_parameter(&blueprint.definitions, &ask) .ask_next_parameter(&blueprint.definitions, &ask)
.map_err(|e| e.into()) .map_err(|e| e.into())
})?; })?;
Ok(term) Ok(data)
} }
pub fn apply_parameter( pub fn apply_parameter(
&self, &self,
title: Option<&String>, title: Option<&String>,
param: &Term<DeBruijn>, param: &PlutusData,
) -> Result<Blueprint, Error> { ) -> Result<Blueprint, Error> {
// Read blueprint // Read blueprint
let blueprint = File::open(self.blueprint_path()) let blueprint = File::open(self.blueprint_path())

View File

@ -1,11 +1,8 @@
use crate::pretty; use crate::pretty;
use aiken_lang::{ use aiken_lang::{
ast::{BinOp, DataTypeKey, Span, TypedDataType, TypedTest}, ast::{Arg, BinOp, DataTypeKey, TypedDataType, TypedTest},
expr::{TypedExpr, UntypedExpr}, expr::UntypedExpr,
gen_uplc::{ gen_uplc::{builder::convert_opaque_type, CodeGenerator},
builder::{convert_data_to_type, convert_opaque_type},
CodeGenerator,
},
tipo::Type, tipo::Type,
}; };
use indexmap::IndexMap; use indexmap::IndexMap;
@ -22,7 +19,6 @@ use std::{
use uplc::{ use uplc::{
ast::{Constant, Data, Name, NamedDeBruijn, Program, Term}, ast::{Constant, Data, Name, NamedDeBruijn, Program, Term},
machine::{cost_model::ExBudget, eval_result::EvalResult}, machine::{cost_model::ExBudget, eval_result::EvalResult},
parser::interner::Interner,
}; };
/// ----- Test ----------------------------------------------------------------- /// ----- Test -----------------------------------------------------------------
@ -94,20 +90,20 @@ impl Test {
input_path: PathBuf, input_path: PathBuf,
) -> Test { ) -> Test {
if test.arguments.is_empty() { if test.arguments.is_empty() {
let program = generator.generate_raw(&test.body, &module_name); let program = generator.generate_raw(&test.body, &[], &module_name);
// TODO: Check whether we really need to clone the _entire_ generator, or whether we // TODO: Check whether we really need to clone the _entire_ generator, or whether we
// can mostly copy the generator and only clone parts that matters. // can mostly copy the generator and only clone parts that matters.
let assertion = test.test_hint().map(|(bin_op, left_src, right_src)| { let assertion = test.test_hint().map(|(bin_op, left_src, right_src)| {
let left = generator let left = generator
.clone() .clone()
.generate_raw(&left_src, &module_name) .generate_raw(&left_src, &[], &module_name)
.try_into() .try_into()
.unwrap(); .unwrap();
let right = generator let right = generator
.clone() .clone()
.generate_raw(&right_src, &module_name) .generate_raw(&right_src, &[], &module_name)
.try_into() .try_into()
.unwrap(); .unwrap();
@ -134,23 +130,21 @@ impl Test {
let type_info = parameter.tipo.clone(); let type_info = parameter.tipo.clone();
// TODO: Possibly refactor 'generate_raw' to accept arguments and do this wrapping let stripped_type_info = convert_opaque_type(&type_info, generator.data_types(), true);
// itself.
let body = TypedExpr::Fn {
location: Span::empty(),
tipo: Rc::new(Type::Fn {
args: vec![type_info.clone()],
ret: test.body.tipo(),
}),
is_capture: false,
args: vec![parameter.into()],
body: Box::new(test.body),
return_annotation: None,
};
let program = generator.clone().generate_raw(&body, &module_name); let program = generator.clone().generate_raw(
&test.body,
&[Arg {
tipo: stripped_type_info.clone(),
..parameter.clone().into()
}],
&module_name,
);
let fuzzer = generator.clone().generate_raw(&via, &module_name); // NOTE: We need not to pass any parameter to the fuzzer here because the fuzzer
// argument is a Data constructor which needs not any conversion. So we can just safely
// apply onto it later.
let fuzzer = generator.clone().generate_raw(&via, &[], &module_name);
Self::property_test( Self::property_test(
input_path, input_path,
@ -160,11 +154,7 @@ impl Test {
program, program,
Fuzzer { Fuzzer {
program: fuzzer, program: fuzzer,
stripped_type_info: convert_opaque_type( stripped_type_info,
&type_info,
generator.data_types(),
true,
),
type_info, type_info,
}, },
) )
@ -277,6 +267,8 @@ impl PropertyTest {
} = next_prng } = next_prng
{ {
if result.failed(self.can_error) { if result.failed(self.can_error) {
println!("{:#?}", result.result());
let mut counterexample = Counterexample { let mut counterexample = Counterexample {
value, value,
choices: next_prng.choices(), choices: next_prng.choices(),
@ -297,12 +289,7 @@ impl PropertyTest {
} }
pub fn eval(&self, value: &PlutusData) -> EvalResult { pub fn eval(&self, value: &PlutusData) -> EvalResult {
let term: Term<Name> = let program = self.program.apply_data(value.clone());
convert_data_to_type(Term::data(value.clone()), &self.fuzzer.stripped_type_info);
let mut program = self.program.apply_term(&term);
Interner::new().program(&mut program);
Program::<NamedDeBruijn>::try_from(program) Program::<NamedDeBruijn>::try_from(program)
.unwrap() .unwrap()

View File

@ -49,7 +49,7 @@ fn assert_uplc(source_code: &str, expected: Term<Name>, should_fail: bool) {
match &script.2 { match &script.2 {
TestType::Func(Function { body: func, .. }) => { TestType::Func(Function { body: func, .. }) => {
let program = generator.generate_raw(func, &script.1); let program = generator.generate_raw(func, &[], &script.1);
let debruijn_program: Program<DeBruijn> = program.try_into().unwrap(); let debruijn_program: Program<DeBruijn> = program.try_into().unwrap();

View File

@ -13,9 +13,8 @@ use num_bigint::BigInt;
use ordinal::Ordinal; use ordinal::Ordinal;
use owo_colors::{OwoColorize, Stream::Stderr}; use owo_colors::{OwoColorize, Stream::Stderr};
use pallas::ledger::primitives::alonzo::PlutusData; use pallas::ledger::primitives::alonzo::PlutusData;
use std::str::FromStr; use std::{fs, path::PathBuf, process, str::FromStr};
use std::{fs, path::PathBuf, process, rc::Rc}; use uplc::ast::Data as UplcData;
use uplc::ast::{Constant, Data as UplcData, DeBruijn, Term};
/// Apply a parameter to a parameterized validator. /// Apply a parameter to a parameterized validator.
#[derive(clap::Args)] #[derive(clap::Args)]
@ -68,7 +67,7 @@ pub fn exec(
.if_supports_color(Stderr, |s| s.bold()), .if_supports_color(Stderr, |s| s.bold()),
); );
let term: Term<DeBruijn> = match &parameter { let data: PlutusData = match &parameter {
Some(param) => { Some(param) => {
eprintln!( eprintln!(
"{} inputs", "{} inputs",
@ -90,7 +89,7 @@ pub fn exec(
process::exit(1) process::exit(1)
}); });
let data = uplc::plutus_data(&bytes) uplc::plutus_data(&bytes)
.map_err::<Error, _>(|e| { .map_err::<Error, _>(|e| {
blueprint::error::Error::MalformedParameter { blueprint::error::Error::MalformedParameter {
hint: format!("Invalid Plutus data; malformed CBOR encoding: {e}"), hint: format!("Invalid Plutus data; malformed CBOR encoding: {e}"),
@ -101,9 +100,7 @@ pub fn exec(
println!(); println!();
e.report(); e.report();
process::exit(1) process::exit(1)
}); })
Term::Constant(Rc::new(Constant::Data(data)))
} }
None => p.construct_parameter_incrementally(title, ask_schema)?, None => p.construct_parameter_incrementally(title, ask_schema)?,
@ -114,16 +111,13 @@ pub fn exec(
" Applying" " Applying"
.if_supports_color(Stderr, |s| s.purple()) .if_supports_color(Stderr, |s| s.purple())
.if_supports_color(Stderr, |s| s.bold()), .if_supports_color(Stderr, |s| s.bold()),
match TryInto::<PlutusData>::try_into(term.clone()) { {
Ok(data) => {
let padding = "\n "; let padding = "\n ";
multiline(48, UplcData::to_hex(data)).join(padding) multiline(48, UplcData::to_hex(data.clone())).join(padding)
}
Err(_) => term.to_pretty(),
} }
); );
let blueprint = p.apply_parameter(title, &term)?; let blueprint = p.apply_parameter(title, &data)?;
let json = serde_json::to_string_pretty(&blueprint).unwrap(); let json = serde_json::to_string_pretty(&blueprint).unwrap();
@ -179,16 +173,21 @@ fn ask_schema(
} }
Schema::Data(Data::List(Items::Many(ref decls))) => { Schema::Data(Data::List(Items::Many(ref decls))) => {
eprintln!(" {}", asking(schema, "Found", &format!("a {}-tuple", decls.len()))); eprintln!(
" {}",
asking(schema, "Found", &format!("a {}-tuple", decls.len()))
);
let mut elems = vec![]; let mut elems = vec![];
for (ix, decl) in decls.iter().enumerate() { for (ix, decl) in decls.iter().enumerate() {
eprintln!( eprintln!(
" {} Tuple's {}{} element", " {} Tuple's {}{} element",
"Asking".if_supports_color(Stderr, |s| s.purple()).if_supports_color(Stderr, |s| s.bold()), "Asking"
ix+1, .if_supports_color(Stderr, |s| s.purple())
Ordinal::<usize>(ix+1).suffix() .if_supports_color(Stderr, |s| s.bold()),
ix + 1,
Ordinal::<usize>(ix + 1).suffix()
); );
let inner_schema = lookup_declaration(&decl.clone().into(), definitions); let inner_schema = lookup_declaration(&decl.clone().into(), definitions);
elems.push(ask_schema(&inner_schema, definitions)?); elems.push(ask_schema(&inner_schema, definitions)?);
@ -252,7 +251,9 @@ fn ask_schema(
Ok(UplcData::constr(ix.try_into().unwrap(), fields)) Ok(UplcData::constr(ix.try_into().unwrap(), fields))
} }
_ => unimplemented!("Hey! You've found a case that we haven't implemented yet. Yes, we've been a bit lazy on that one... If that use-case is important to you, please let us know on Discord or on Github."), _ => unimplemented!(
"Hey! You've found a case that we haven't implemented yet. Yes, we've been a bit lazy on that one... If that use-case is important to you, please let us know on Discord or on Github."
),
} }
} }

View File

@ -30,39 +30,41 @@ pub fn exec(
cbor, cbor,
}: Args, }: Args,
) -> miette::Result<()> { ) -> miette::Result<()> {
let mut program = if cbor { let mut program: Program<Name> = if cbor {
let cbor_hex = std::fs::read_to_string(&script).into_diagnostic()?; let cbor_hex = std::fs::read_to_string(&script).into_diagnostic()?;
let raw_cbor = hex::decode(cbor_hex.trim()).into_diagnostic()?; let raw_cbor = hex::decode(cbor_hex.trim()).into_diagnostic()?;
let prog = Program::<FakeNamedDeBruijn>::from_cbor(&raw_cbor, &mut Vec::new()) let program = Program::<FakeNamedDeBruijn>::from_cbor(&raw_cbor, &mut Vec::new())
.into_diagnostic()?; .into_diagnostic()?;
prog.into() let program: Program<NamedDeBruijn> = program.into();
Program::<Name>::try_from(program).into_diagnostic()?
} else if flat { } else if flat {
let bytes = std::fs::read(&script).into_diagnostic()?; let bytes = std::fs::read(&script).into_diagnostic()?;
let prog = Program::<FakeNamedDeBruijn>::from_flat(&bytes).into_diagnostic()?; let program = Program::<FakeNamedDeBruijn>::from_flat(&bytes).into_diagnostic()?;
prog.into() let program: Program<NamedDeBruijn> = program.into();
Program::<Name>::try_from(program).into_diagnostic()?
} else { } else {
let code = std::fs::read_to_string(&script).into_diagnostic()?; let code = std::fs::read_to_string(&script).into_diagnostic()?;
let prog = parser::program(&code).into_diagnostic()?; parser::program(&code).into_diagnostic()?
Program::<NamedDeBruijn>::try_from(prog).into_diagnostic()?
}; };
for arg in args { for arg in args {
let term = parser::term(&arg).into_diagnostic()?; let term = parser::term(&arg).into_diagnostic()?;
let term = Term::<NamedDeBruijn>::try_from(term).into_diagnostic()?; program = program.apply_term(&term)
program = program.apply_term(&term);
} }
let budget = ExBudget::default(); let budget = ExBudget::default();
let program = Program::<NamedDeBruijn>::try_from(program).into_diagnostic()?;
let mut eval_result = program.eval(budget); let mut eval_result = program.eval(budget);
let cost = eval_result.cost(); let cost = eval_result.cost();

View File

@ -7,6 +7,7 @@ use crate::{
eval_result::EvalResult, eval_result::EvalResult,
Machine, Machine,
}, },
parser::interner::Interner,
}; };
use num_bigint::BigInt; use num_bigint::BigInt;
use num_traits::ToPrimitive; use num_traits::ToPrimitive;
@ -18,7 +19,6 @@ use pallas::ledger::{
}, },
traverse::ComputeHash, traverse::ComputeHash,
}; };
use serde::{ use serde::{
self, self,
de::{self, Deserialize, Deserializer, MapAccess, Visitor}, de::{self, Deserialize, Deserializer, MapAccess, Visitor},
@ -58,21 +58,8 @@ where
} }
} }
/// We use this to apply the validator to Datum, /// A convenient and faster version that `apply_term` since the program doesn't need to be
/// then redeemer, then ScriptContext. If datum is /// re-interned (constant Data do not introduce new bindings).
/// even necessary (i.e. minting policy).
pub fn apply_term(&self, term: &Term<T>) -> Self {
let applied_term = Term::Apply {
function: Rc::new(self.term.clone()),
argument: Rc::new(term.clone()),
};
Program {
version: self.version,
term: applied_term,
}
}
pub fn apply_data(&self, plutus_data: PlutusData) -> Self { pub fn apply_data(&self, plutus_data: PlutusData) -> Self {
let applied_term = Term::Apply { let applied_term = Term::Apply {
function: Rc::new(self.term.clone()), function: Rc::new(self.term.clone()),
@ -86,6 +73,27 @@ where
} }
} }
impl Program<Name> {
/// We use this to apply the validator to Datum,
/// then redeemer, then ScriptContext. If datum is
/// even necessary (i.e. minting policy).
pub fn apply_term(&self, term: &Term<Name>) -> Self {
let applied_term = Term::Apply {
function: Rc::new(self.term.clone()),
argument: Rc::new(term.clone()),
};
let mut program = Program {
version: self.version,
term: applied_term,
};
Interner::new().program(&mut program);
program
}
}
impl<'a, T> Display for Program<T> impl<'a, T> Display for Program<T>
where where
T: Binder<'a>, T: Binder<'a>,