Merge pull request #166 from aiken-lang/some-interesting-test-cases

Include generics to get test cases working
This commit is contained in:
Matthias Benkort
2022-12-15 02:07:05 +01:00
committed by GitHub
46 changed files with 2760 additions and 1332 deletions

View File

@@ -1,13 +1,18 @@
use crate::{pretty, script::EvalHint};
use aiken_lang::{
ast::{BinOp, Span},
parser::error::ParseError,
tipo,
};
use miette::{
Diagnostic, EyreContext, LabeledSpan, MietteHandlerOpts, NamedSource, RgbColors, SourceCode,
};
use std::{
fmt::{Debug, Display},
io,
path::{Path, PathBuf},
};
use aiken_lang::{ast::Span, parser::error::ParseError, tipo};
use miette::{
Diagnostic, EyreContext, LabeledSpan, MietteHandlerOpts, NamedSource, RgbColors, SourceCode,
};
use uplc::machine::cost_model::ExBudget;
#[allow(dead_code)]
#[derive(thiserror::Error)]
@@ -28,7 +33,7 @@ pub enum Error {
#[error(transparent)]
StandardIo(#[from] io::Error),
#[error("Syclical module imports")]
#[error("Cyclical module imports")]
ImportCycle { modules: Vec<String> },
/// Useful for returning many [`Error::Parse`] at once
@@ -73,6 +78,15 @@ pub enum Error {
src: String,
named: NamedSource,
},
#[error("{name} failed{}", if *verbose { format!("\n{src}") } else { String::new() } )]
TestFailure {
name: String,
path: PathBuf,
verbose: bool,
src: String,
evaluation_hint: Option<EvalHint>,
},
}
impl Error {
@@ -148,6 +162,7 @@ impl Error {
Error::Type { path, .. } => Some(path.to_path_buf()),
Error::ValidatorMustReturnBool { path, .. } => Some(path.to_path_buf()),
Error::WrongValidatorArity { path, .. } => Some(path.to_path_buf()),
Error::TestFailure { path, .. } => Some(path.to_path_buf()),
}
}
@@ -163,6 +178,7 @@ impl Error {
Error::Type { src, .. } => Some(src.to_string()),
Error::ValidatorMustReturnBool { src, .. } => Some(src.to_string()),
Error::WrongValidatorArity { src, .. } => Some(src.to_string()),
Error::TestFailure { .. } => None,
}
}
}
@@ -203,6 +219,7 @@ impl Diagnostic for Error {
Error::Format { .. } => None,
Error::ValidatorMustReturnBool { .. } => Some(Box::new("aiken::scripts")),
Error::WrongValidatorArity { .. } => Some(Box::new("aiken::validators")),
Error::TestFailure { path, .. } => Some(Box::new(path.to_str().unwrap_or(""))),
}
}
@@ -225,6 +242,34 @@ impl Diagnostic for Error {
Error::Format { .. } => None,
Error::ValidatorMustReturnBool { .. } => Some(Box::new("Try annotating the validator's return type with Bool")),
Error::WrongValidatorArity { .. } => Some(Box::new("Validators require a minimum number of arguments please add the missing arguments.\nIf you don't need one of the required arguments use an underscore `_datum`.")),
Error::TestFailure { evaluation_hint, .. } =>{
match evaluation_hint {
None => None,
Some(hint) => {
let budget = ExBudget { mem: i64::MAX, cpu: i64::MAX, };
let left = pretty::boxed("left", match hint.left.eval(budget) {
(Ok(term), _, _) => format!("{term}"),
(Err(err), _, _) => format!("{err}"),
});
let right = pretty::boxed("right", match hint.right.eval(budget) {
(Ok(term), _, _) => format!("{term}"),
(Err(err), _, _) => format!("{err}"),
});
let msg = match hint.bin_op {
BinOp::And => Some(format!("{left}\n\nand\n\n{right}\n\nshould both be true.")),
BinOp::Or => Some(format!("{left}\n\nor\n\n{right}\n\nshould be true.")),
BinOp::Eq => Some(format!("{left}\n\nshould be equal to\n\n{right}")),
BinOp::NotEq => Some(format!("{left}\n\nshould not be equal to\n\n{right}")),
BinOp::LtInt => Some(format!("{left}\n\nshould be lower than\n\n{right}")),
BinOp::LtEqInt => Some(format!("{left}\n\nshould be lower than or equal to\n\n{right}")),
BinOp::GtEqInt => Some(format!("{left}\n\nshould be greater than\n\n{right}")),
BinOp::GtInt => Some(format!("{left}\n\nshould be greater than or equal to\n\n{right}")),
_ => None
}?;
Some(Box::new(msg))
}
}
},
}
}
@@ -244,6 +289,7 @@ impl Diagnostic for Error {
Error::WrongValidatorArity { location, .. } => Some(Box::new(
vec![LabeledSpan::new_with_span(None, *location)].into_iter(),
)),
Error::TestFailure { .. } => None,
}
}
@@ -259,6 +305,7 @@ impl Diagnostic for Error {
Error::Format { .. } => None,
Error::ValidatorMustReturnBool { named, .. } => Some(named),
Error::WrongValidatorArity { named, .. } => Some(named),
Error::TestFailure { .. } => None,
}
}
}

View File

@@ -1,22 +1,21 @@
use std::{
collections::HashMap,
fs,
path::{Path, PathBuf},
};
pub mod config;
pub mod error;
pub mod format;
pub mod module;
pub mod options;
pub mod pretty;
pub mod script;
pub mod telemetry;
use aiken_lang::{
ast::{Definition, Function, ModuleKind, TypedFunction},
builtins,
ast::{
Annotation, DataType, Definition, Function, ModuleKind, RecordConstructor,
RecordConstructorArg, Span, TypedDataType, TypedDefinition, TypedFunction,
},
builder::{DataTypeKey, FunctionAccessKey},
builtins::{self, generic_var},
tipo::TypeInfo,
uplc::{CodeGenerator, DataTypeKey, FunctionAccessKey},
uplc::CodeGenerator,
IdGenerator,
};
use miette::NamedSource;
@@ -26,11 +25,16 @@ use pallas::{
ledger::{addresses::Address, primitives::babbage},
};
use pallas_traverse::ComputeHash;
use script::Script;
use script::{EvalHint, EvalInfo, Script};
use serde_json::json;
use telemetry::{EventListener, TestInfo};
use std::{
collections::HashMap,
fs,
path::{Path, PathBuf},
};
use telemetry::EventListener;
use uplc::{
ast::{DeBruijn, Program},
ast::{Constant, DeBruijn, Program, Term},
machine::cost_model::ExBudget,
};
@@ -101,12 +105,20 @@ where
self.compile(options)
}
pub fn check(&mut self, skip_tests: bool, match_tests: Option<String>) -> Result<(), Error> {
pub fn check(
&mut self,
skip_tests: bool,
match_tests: Option<String>,
verbose: bool,
) -> Result<(), Error> {
let options = Options {
code_gen_mode: if skip_tests {
CodeGenMode::NoOp
} else {
CodeGenMode::Test(match_tests)
CodeGenMode::Test {
match_tests,
verbose,
}
},
};
@@ -140,19 +152,47 @@ where
self.event_listener.handle_event(Event::GeneratingUPLC {
output_path: self.output_path(),
});
let programs = self.code_gen(validators, &checked_modules)?;
self.write_build_outputs(programs, uplc_dump)?;
Ok(())
}
CodeGenMode::Test(match_tests) => {
let tests = self.test_gen(&checked_modules)?;
self.run_tests(tests, match_tests);
}
CodeGenMode::NoOp => (),
}
CodeGenMode::Test {
match_tests,
verbose,
} => {
let tests = self
.collect_scripts(&checked_modules, |def| matches!(def, Definition::Test(..)))?;
if !tests.is_empty() {
self.event_listener.handle_event(Event::RunningTests);
}
let results = self.eval_scripts(tests, match_tests);
let errors: Vec<Error> = results
.iter()
.filter_map(|e| {
if e.success {
None
} else {
Some(Error::TestFailure {
name: e.script.name.clone(),
path: e.script.input_path.clone(),
evaluation_hint: e.script.evaluation_hint.clone(),
src: e.script.program.to_pretty(),
verbose,
})
}
})
.collect();
Ok(())
self.event_listener
.handle_event(Event::FinishedTests { tests: results });
if !errors.is_empty() {
Err(Error::List(errors))
} else {
Ok(())
}
}
CodeGenMode::NoOp => Ok(()),
}
}
fn read_source_files(&mut self) -> Result<(), Error> {
@@ -290,7 +330,7 @@ where
fn validate_validators(
&self,
checked_modules: &mut CheckedModules,
) -> Result<Vec<(String, TypedFunction)>, Error> {
) -> Result<Vec<(PathBuf, String, TypedFunction)>, Error> {
let mut errors = Vec::new();
let mut validators = Vec::new();
let mut indices_to_remove = Vec::new();
@@ -344,7 +384,11 @@ where
})
}
validators.push((module.name.clone(), func_def.clone()));
validators.push((
module.input_path.clone(),
module.name.clone(),
func_def.clone(),
));
indices_to_remove.push(index);
}
}
@@ -364,7 +408,7 @@ where
fn code_gen(
&mut self,
validators: Vec<(String, TypedFunction)>,
validators: Vec<(PathBuf, String, TypedFunction)>,
checked_modules: &CheckedModules,
) -> Result<Vec<Script>, Error> {
let mut programs = Vec::new();
@@ -374,6 +418,16 @@ where
let mut imports = HashMap::new();
let mut constants = HashMap::new();
let option_data_type = make_option();
data_types.insert(
DataTypeKey {
module_name: "".to_string(),
defined_type: "Option".to_string(),
},
&option_data_type,
);
for module in checked_modules.values() {
for def in module.ast.definitions() {
match def {
@@ -382,6 +436,7 @@ where
FunctionAccessKey {
module_name: module.name.clone(),
function_name: func.name.clone(),
variant_name: String::new(),
},
func,
);
@@ -409,7 +464,7 @@ where
}
}
for (module_name, func_def) in validators {
for (input_path, module_name, func_def) in validators {
let Function {
arguments,
name,
@@ -426,9 +481,15 @@ where
&self.module_types,
);
let program = generator.generate(body, arguments);
let program = generator.generate(body, arguments, true);
let script = Script::new(module_name, name, program.try_into().unwrap());
let script = Script::new(
input_path,
module_name,
name,
program.try_into().unwrap(),
None,
);
programs.push(script);
}
@@ -437,7 +498,11 @@ where
}
// TODO: revisit ownership and lifetimes of data in this function
fn test_gen(&mut self, checked_modules: &CheckedModules) -> Result<Vec<Script>, Error> {
fn collect_scripts(
&mut self,
checked_modules: &CheckedModules,
should_collect: fn(&TypedDefinition) -> bool,
) -> Result<Vec<Script>, Error> {
let mut programs = Vec::new();
let mut functions = HashMap::new();
let mut type_aliases = HashMap::new();
@@ -445,8 +510,18 @@ where
let mut imports = HashMap::new();
let mut constants = HashMap::new();
let option_data_type = make_option();
data_types.insert(
DataTypeKey {
module_name: "".to_string(),
defined_type: "Option".to_string(),
},
&option_data_type,
);
// let mut indices_to_remove = Vec::new();
let mut tests = Vec::new();
let mut scripts = Vec::new();
for module in checked_modules.values() {
for (_index, def) in module.ast.definitions().enumerate() {
@@ -456,12 +531,18 @@ where
FunctionAccessKey {
module_name: module.name.clone(),
function_name: func.name.clone(),
variant_name: String::new(),
},
func,
);
if should_collect(def) {
scripts.push((module.input_path.clone(), module.name.clone(), func));
}
}
Definition::Test(func) => {
tests.push((module.name.clone(), func));
if should_collect(def) {
scripts.push((module.input_path.clone(), module.name.clone(), func));
}
// indices_to_remove.push(index);
}
Definition::TypeAlias(ta) => {
@@ -490,7 +571,7 @@ where
// }
}
for (module_name, func_def) in tests {
for (input_path, module_name, func_def) in scripts {
let Function {
arguments,
name,
@@ -507,9 +588,34 @@ where
&self.module_types,
);
let program = generator.generate(body.clone(), arguments.clone());
let evaluation_hint = if let Some((bin_op, left_src, right_src)) = func_def.test_hint()
{
let left = CodeGenerator::new(&functions, &data_types, &self.module_types)
.generate(*left_src, vec![], false)
.try_into()
.unwrap();
let right = CodeGenerator::new(&functions, &data_types, &self.module_types)
.generate(*right_src, vec![], false)
.try_into()
.unwrap();
Some(EvalHint {
bin_op,
left,
right,
})
} else {
None
};
let script = Script::new(module_name, name.to_string(), program.try_into().unwrap());
let program = generator.generate(body.clone(), arguments.clone(), false);
let script = Script::new(
input_path,
module_name,
name.to_string(),
program.try_into().unwrap(),
evaluation_hint,
);
programs.push(script);
}
@@ -517,7 +623,7 @@ where
Ok(programs)
}
fn run_tests(&self, tests: Vec<Script>, match_tests: Option<String>) {
fn eval_scripts(&self, scripts: Vec<Script>, match_name: Option<String>) -> Vec<EvalInfo> {
// TODO: in the future we probably just want to be able to
// tell the machine to not explode on budget consumption.
let initial_budget = ExBudget {
@@ -525,43 +631,41 @@ where
cpu: i64::MAX,
};
if !tests.is_empty() {
self.event_listener.handle_event(Event::RunningTests);
}
let mut results = Vec::new();
for test in tests {
let path = format!("{}{}", test.module, test.name);
for script in scripts {
let path = format!("{}{}", script.module, script.name);
if matches!(&match_tests, Some(search_str) if !path.to_string().contains(search_str)) {
if matches!(&match_name, Some(search_str) if !path.to_string().contains(search_str)) {
continue;
}
match test.program.eval(initial_budget) {
(Ok(..), remaining_budget, _) => {
let test_info = TestInfo {
is_passing: true,
test,
match script.program.eval(initial_budget) {
(Ok(result), remaining_budget, _) => {
let eval_info = EvalInfo {
success: result != Term::Error
&& result != Term::Constant(Constant::Bool(false)),
script,
spent_budget: initial_budget - remaining_budget,
output: Some(result),
};
results.push(test_info);
results.push(eval_info);
}
(Err(_), remaining_budget, _) => {
let test_info = TestInfo {
is_passing: false,
test,
(Err(..), remaining_budget, _) => {
let eval_info = EvalInfo {
success: false,
script,
spent_budget: initial_budget - remaining_budget,
output: None,
};
results.push(test_info);
results.push(eval_info);
}
}
}
self.event_listener
.handle_event(Event::FinishedTests { tests: results });
results
}
fn output_path(&self) -> PathBuf {
@@ -722,3 +826,40 @@ fn is_aiken_path(path: &Path, dir: impl AsRef<Path>) -> bool {
.expect("is_aiken_path(): to_str"),
)
}
fn make_option() -> TypedDataType {
DataType {
constructors: vec![
RecordConstructor {
location: Span::empty(),
name: "Some".to_string(),
arguments: vec![RecordConstructorArg {
label: None,
annotation: Annotation::Var {
location: Span::empty(),
name: "a".to_string(),
},
location: Span::empty(),
tipo: generic_var(0),
doc: None,
}],
documentation: None,
sugar: false,
},
RecordConstructor {
location: Span::empty(),
name: "None".to_string(),
arguments: vec![],
documentation: None,
sugar: false,
},
],
doc: None,
location: Span::empty(),
name: "Option".to_string(),
opaque: false,
parameters: vec!["a".to_string()],
public: true,
typed_parameters: vec![generic_var(0)],
}
}

View File

@@ -3,7 +3,10 @@ pub struct Options {
}
pub enum CodeGenMode {
Test(Option<String>),
Test {
match_tests: Option<String>,
verbose: bool,
},
Build(bool),
NoOp,
}

View File

@@ -0,0 +1,48 @@
pub fn boxed(title: &str, content: String) -> String {
let n = content.lines().fold(0, |max, l| {
let n = l.len();
if n > max {
n
} else {
max
}
});
let content = content
.lines()
.map(|line| format!("{}", pad_right(line.to_string(), n, " ")))
.collect::<Vec<String>>()
.join("\n");
let top = format!("┍━ {}", pad_right(format!("{title} "), n, ""));
let bottom = format!("{}", pad_right(String::new(), n + 2, ""));
format!("{top}\n{content}\n{bottom}")
}
pub fn pad_left(mut text: String, n: usize, delimiter: &str) -> String {
let diff = n as i32 - text.len() as i32;
if diff.is_positive() {
for _ in 0..diff {
text.insert_str(0, delimiter);
}
}
text
}
pub fn pad_right(mut text: String, n: usize, delimiter: &str) -> String {
let diff = n as i32 - text.len() as i32;
if diff.is_positive() {
for _ in 0..diff {
text.push_str(delimiter);
}
}
text
}
pub fn style_if(styled: bool, s: String, apply_style: fn(String) -> String) -> String {
if styled {
apply_style(s)
} else {
s
}
}

View File

@@ -1,18 +1,46 @@
use crate::{ExBudget, Term};
use aiken_lang::ast::BinOp;
use std::path::PathBuf;
use uplc::ast::{NamedDeBruijn, Program};
#[derive(Debug)]
pub struct Script {
pub input_path: PathBuf,
pub module: String,
pub name: String,
pub program: Program<NamedDeBruijn>,
pub evaluation_hint: Option<EvalHint>,
}
impl Script {
pub fn new(module: String, name: String, program: Program<NamedDeBruijn>) -> Script {
pub fn new(
input_path: PathBuf,
module: String,
name: String,
program: Program<NamedDeBruijn>,
evaluation_hint: Option<EvalHint>,
) -> Script {
Script {
input_path,
module,
name,
program,
evaluation_hint,
}
}
}
#[derive(Debug, Clone)]
pub struct EvalHint {
pub bin_op: BinOp,
pub left: Program<NamedDeBruijn>,
pub right: Program<NamedDeBruijn>,
}
#[derive(Debug)]
pub struct EvalInfo {
pub success: bool,
pub script: Script,
pub spent_budget: ExBudget,
pub output: Option<Term<NamedDeBruijn>>,
}

View File

@@ -1,6 +1,5 @@
use crate::script::Script;
use crate::script::EvalInfo;
use std::path::PathBuf;
use uplc::machine::cost_model::ExBudget;
pub trait EventListener: std::fmt::Debug {
fn handle_event(&self, event: Event);
@@ -17,14 +16,11 @@ pub enum Event {
GeneratingUPLC {
output_path: PathBuf,
},
EvaluatingFunction {
results: Vec<EvalInfo>,
},
RunningTests,
FinishedTests {
tests: Vec<TestInfo>,
tests: Vec<EvalInfo>,
},
}
pub struct TestInfo {
pub is_passing: bool,
pub test: Script,
pub spent_budget: ExBudget,
}