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
commit d9d1310c6d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
46 changed files with 2760 additions and 1332 deletions

View File

@ -1,9 +1,13 @@
root = true root = true
[*.ak]
indent_style = space
indent_size = 2
end_of_line = lf end_of_line = lf
charset = utf-8 charset = utf-8
trim_trailing_whitespace = true trim_trailing_whitespace = true
insert_final_newline = true insert_final_newline = true
[*.ak]
indent_style = space
indent_size = 2
[Makefile]
indent_style = tabs
indent_size = 4

View File

@ -11,6 +11,10 @@ pub struct Args {
#[clap(short, long)] #[clap(short, long)]
skip_tests: bool, skip_tests: bool,
/// When enabled, also pretty-print test UPLC on failure
#[clap(long)]
debug: bool,
/// Only run tests if their path + name match the given string /// Only run tests if their path + name match the given string
#[clap(short, long)] #[clap(short, long)]
match_tests: Option<String>, match_tests: Option<String>,
@ -20,8 +24,11 @@ pub fn exec(
Args { Args {
directory, directory,
skip_tests, skip_tests,
debug,
match_tests, match_tests,
}: Args, }: Args,
) -> miette::Result<()> { ) -> miette::Result<()> {
crate::with_project(directory, |p| p.check(skip_tests, match_tests.clone())) crate::with_project(directory, |p| {
p.check(skip_tests, match_tests.clone(), debug)
})
} }

View File

@ -1,10 +1,7 @@
use std::{env, path::PathBuf}; use std::collections::BTreeMap;
use std::{env, path::PathBuf, process};
use aiken_project::{ use aiken_project::{config::Config, pretty, script::EvalInfo, telemetry, Project};
config::Config,
telemetry::{self, TestInfo},
Project,
};
use miette::IntoDiagnostic; use miette::IntoDiagnostic;
use owo_colors::OwoColorize; use owo_colors::OwoColorize;
use uplc::machine::cost_model::ExBudget; use uplc::machine::cost_model::ExBudget;
@ -35,12 +32,20 @@ where
if let Err(err) = build_result { if let Err(err) = build_result {
err.report(); err.report();
println!("{}", "Summary".purple().bold());
miette::bail!("Failed: {} error(s), {warning_count} warning(s)", err.len(),); println!(
}; " {} error(s), {}",
err.len(),
println!("\nFinished with {warning_count} warning(s)\n"); format!("{warning_count} warning(s)").yellow(),
);
process::exit(1);
} else {
println!("{}", "Summary".purple().bold());
println!(
" 0 error, {}",
format!("{warning_count} warning(s)").yellow(),
);
}
Ok(()) Ok(())
} }
@ -76,89 +81,149 @@ impl telemetry::EventListener for Terminal {
output_path.to_str().unwrap_or("").bright_blue() output_path.to_str().unwrap_or("").bright_blue()
); );
} }
telemetry::Event::EvaluatingFunction { results } => {
println!("{}\n", "...Evaluating function".bold().purple());
let (max_mem, max_cpu) = find_max_execution_units(&results);
for eval_info in &results {
println!(" {}", fmt_eval(eval_info, max_mem, max_cpu))
}
}
telemetry::Event::RunningTests => { telemetry::Event::RunningTests => {
println!("{}\n", "...Running tests".bold().purple()); println!("{}\n", "...Running tests".bold().purple());
} }
telemetry::Event::FinishedTests { tests } => { telemetry::Event::FinishedTests { tests } => {
let (max_mem, max_cpu) = tests.iter().fold( let (max_mem, max_cpu) = find_max_execution_units(&tests);
(0, 0),
|(max_mem, max_cpu), TestInfo { spent_budget, .. }| {
if spent_budget.mem >= max_mem && spent_budget.cpu >= max_cpu {
(spent_budget.mem, spent_budget.cpu)
} else if spent_budget.mem > max_mem {
(spent_budget.mem, max_cpu)
} else if spent_budget.cpu > max_cpu {
(max_mem, spent_budget.cpu)
} else {
(max_mem, max_cpu)
}
},
);
let max_mem = max_mem.to_string().len() as i32; for (module, infos) in &group_by_module(&tests) {
let max_cpu = max_cpu.to_string().len() as i32; let first = fmt_test(infos.first().unwrap(), max_mem, max_cpu, false).len();
println!(
for test_info in &tests { "{} {} {}",
println!("{}", fmt_test(test_info, max_mem, max_cpu)) " ┌──".bright_black(),
module.bold().blue(),
pretty::pad_left("".to_string(), first - module.len() - 3, "")
.bright_black()
);
for eval_info in infos {
println!(
" {} {}",
"".bright_black(),
fmt_test(eval_info, max_mem, max_cpu, true)
)
}
let last = fmt_test(infos.last().unwrap(), max_mem, max_cpu, false).len();
let summary = fmt_test_summary(infos, false).len();
println!(
"{} {}\n",
pretty::pad_right("".to_string(), last - summary + 5, "")
.bright_black(),
fmt_test_summary(infos, true),
);
} }
let (n_passed, n_failed) =
tests
.iter()
.fold((0, 0), |(n_passed, n_failed), test_info| {
if test_info.is_passing {
(n_passed + 1, n_failed)
} else {
(n_passed, n_failed + 1)
}
});
println!(
"{}",
format!(
"\n Summary: {} test(s), {}; {}.",
tests.len(),
format!("{} passed", n_passed).bright_green(),
format!("{} failed", n_failed).bright_red()
)
.bold()
)
} }
} }
} }
} }
fn fmt_test(test_info: &TestInfo, max_mem: i32, max_cpu: i32) -> String { fn fmt_test(eval_info: &EvalInfo, max_mem: usize, max_cpu: usize, styled: bool) -> String {
let TestInfo { let EvalInfo {
is_passing, success,
test, script,
spent_budget, spent_budget,
} = test_info; ..
} = eval_info;
let ExBudget { mem, cpu } = spent_budget;
let mem_pad = pretty::pad_left(mem.to_string(), max_mem, " ");
let cpu_pad = pretty::pad_left(cpu.to_string(), max_cpu, " ");
format!(
"{} [mem: {}, cpu: {}] {}",
if *success {
pretty::style_if(styled, "PASS".to_string(), |s| s.bold().green().to_string())
} else {
pretty::style_if(styled, "FAIL".to_string(), |s| s.bold().red().to_string())
},
pretty::style_if(styled, mem_pad, |s| s.bright_white().to_string()),
pretty::style_if(styled, cpu_pad, |s| s.bright_white().to_string()),
pretty::style_if(styled, script.name.clone(), |s| s.bright_blue().to_string()),
)
}
fn fmt_test_summary(tests: &Vec<&EvalInfo>, styled: bool) -> String {
let (n_passed, n_failed) = tests
.iter()
.fold((0, 0), |(n_passed, n_failed), test_info| {
if test_info.success {
(n_passed + 1, n_failed)
} else {
(n_passed, n_failed + 1)
}
});
format!(
"{} | {} | {}",
pretty::style_if(styled, format!("{} tests", tests.len()), |s| s
.bold()
.to_string()),
pretty::style_if(styled, format!("{} passed", n_passed), |s| s
.bright_green()
.bold()
.to_string()),
pretty::style_if(styled, format!("{} failed", n_failed), |s| s
.bright_red()
.bold()
.to_string()),
)
}
fn fmt_eval(eval_info: &EvalInfo, max_mem: usize, max_cpu: usize) -> String {
let EvalInfo {
output,
script,
spent_budget,
..
} = eval_info;
let ExBudget { mem, cpu } = spent_budget; let ExBudget { mem, cpu } = spent_budget;
format!( format!(
" [{}] [mem: {}, cpu: {}] {}::{}", " {}::{} [mem: {}, cpu: {}]\n\n ╰─▶ {}",
if *is_passing { script.module.blue(),
"PASS".bold().green().to_string() script.name.bright_blue(),
} else { pretty::pad_left(mem.to_string(), max_mem, " "),
"FAIL".bold().red().to_string() pretty::pad_left(cpu.to_string(), max_cpu, " "),
}, output
pad_left(mem.to_string(), max_mem, " "), .as_ref()
pad_left(cpu.to_string(), max_cpu, " "), .map(|x| format!("{}", x))
test.module.blue(), .unwrap_or_else(|| "Error.".to_string()),
test.name.bright_blue()
) )
} }
fn pad_left(mut text: String, n: i32, delimiter: &str) -> String { fn group_by_module(infos: &Vec<EvalInfo>) -> BTreeMap<String, Vec<&EvalInfo>> {
let diff = n - text.len() as i32; let mut modules = BTreeMap::new();
for eval_info in infos {
if diff.is_positive() { let xs: &mut Vec<&EvalInfo> = modules.entry(eval_info.script.module.clone()).or_default();
for _ in 0..diff { xs.push(eval_info);
text.insert_str(0, delimiter);
}
} }
modules
text }
fn find_max_execution_units(xs: &[EvalInfo]) -> (usize, usize) {
let (max_mem, max_cpu) = xs.iter().fold(
(0, 0),
|(max_mem, max_cpu), EvalInfo { spent_budget, .. }| {
if spent_budget.mem >= max_mem && spent_budget.cpu >= max_cpu {
(spent_budget.mem, spent_budget.cpu)
} else if spent_budget.mem > max_mem {
(spent_budget.mem, max_cpu)
} else if spent_budget.cpu > max_cpu {
(max_mem, spent_budget.cpu)
} else {
(max_mem, max_cpu)
}
},
);
(max_mem.to_string().len(), max_cpu.to_string().len())
} }

View File

@ -28,16 +28,13 @@ pub enum Air {
scope: Vec<u64>, scope: Vec<u64>,
constructor: ValueConstructor, constructor: ValueConstructor,
name: String, name: String,
variant_name: String,
}, },
// Fn { Fn {
// scope: Vec<u64>, scope: Vec<u64>,
// tipo: Arc<Type>, params: Vec<String>,
// is_capture: bool, },
// args: Vec<Arg<Arc<Type>>>,
// body: Box<Self>,
// return_annotation: Option<Annotation>,
// },
List { List {
scope: Vec<u64>, scope: Vec<u64>,
count: usize, count: usize,
@ -67,6 +64,7 @@ pub enum Air {
Builtin { Builtin {
scope: Vec<u64>, scope: Vec<u64>,
func: DefaultFunction, func: DefaultFunction,
tipo: Arc<Type>,
}, },
BinOp { BinOp {
@ -88,6 +86,7 @@ pub enum Air {
module_name: String, module_name: String,
params: Vec<String>, params: Vec<String>,
recursive: bool, recursive: bool,
variant_name: String,
}, },
DefineConst { DefineConst {
@ -237,6 +236,7 @@ impl Air {
| Air::List { scope, .. } | Air::List { scope, .. }
| Air::ListAccessor { scope, .. } | Air::ListAccessor { scope, .. }
| Air::ListExpose { scope, .. } | Air::ListExpose { scope, .. }
| Air::Fn { scope, .. }
| Air::Call { scope, .. } | Air::Call { scope, .. }
| Air::Builtin { scope, .. } | Air::Builtin { scope, .. }
| Air::BinOp { scope, .. } | Air::BinOp { scope, .. }

View File

@ -66,10 +66,8 @@ impl UntypedModule {
} }
} }
pub type TypedDefinition = Definition<Arc<Type>, TypedExpr, String, String>;
pub type UntypedDefinition = Definition<(), UntypedExpr, (), ()>;
pub type TypedFunction = Function<Arc<Type>, TypedExpr>; pub type TypedFunction = Function<Arc<Type>, TypedExpr>;
pub type UntypedFunction = Function<(), UntypedExpr>;
#[derive(Debug, Clone, PartialEq)] #[derive(Debug, Clone, PartialEq)]
pub struct Function<T, Expr> { pub struct Function<T, Expr> {
@ -84,6 +82,24 @@ pub struct Function<T, Expr> {
pub end_position: usize, pub end_position: usize,
} }
pub type TypedTypeAlias = TypeAlias<Arc<Type>>;
pub type UntypedTypeAlias = TypeAlias<()>;
impl TypedFunction {
pub fn test_hint(&self) -> Option<(BinOp, Box<TypedExpr>, Box<TypedExpr>)> {
match &self.body {
TypedExpr::BinOp {
name,
tipo,
left,
right,
..
} if tipo == &bool() => Some((*name, left.clone(), right.clone())),
_ => None,
}
}
}
#[derive(Debug, Clone, PartialEq)] #[derive(Debug, Clone, PartialEq)]
pub struct TypeAlias<T> { pub struct TypeAlias<T> {
pub alias: String, pub alias: String,
@ -95,6 +111,9 @@ pub struct TypeAlias<T> {
pub tipo: T, pub tipo: T,
} }
pub type TypedDataType = DataType<Arc<Type>>;
pub type UntypedDataType = DataType<()>;
#[derive(Debug, Clone, PartialEq)] #[derive(Debug, Clone, PartialEq)]
pub struct DataType<T> { pub struct DataType<T> {
pub constructors: Vec<RecordConstructor<T>>, pub constructors: Vec<RecordConstructor<T>>,
@ -107,6 +126,9 @@ pub struct DataType<T> {
pub typed_parameters: Vec<T>, pub typed_parameters: Vec<T>,
} }
pub type TypedUse = Use<String>;
pub type UntypedUse = Use<()>;
#[derive(Debug, Clone, PartialEq, Eq)] #[derive(Debug, Clone, PartialEq, Eq)]
pub struct Use<PackageName> { pub struct Use<PackageName> {
pub as_name: Option<String>, pub as_name: Option<String>,
@ -116,6 +138,9 @@ pub struct Use<PackageName> {
pub unqualified: Vec<UnqualifiedImport>, pub unqualified: Vec<UnqualifiedImport>,
} }
pub type TypedModuleConstant = ModuleConstant<Arc<Type>, String>;
pub type UntypedModuleConstant = ModuleConstant<(), ()>;
#[derive(Debug, Clone, PartialEq)] #[derive(Debug, Clone, PartialEq)]
pub struct ModuleConstant<T, ConstantRecordTag> { pub struct ModuleConstant<T, ConstantRecordTag> {
pub doc: Option<String>, pub doc: Option<String>,
@ -127,6 +152,9 @@ pub struct ModuleConstant<T, ConstantRecordTag> {
pub tipo: T, pub tipo: T,
} }
pub type TypedDefinition = Definition<Arc<Type>, TypedExpr, String, String>;
pub type UntypedDefinition = Definition<(), UntypedExpr, (), ()>;
#[derive(Debug, Clone, PartialEq)] #[derive(Debug, Clone, PartialEq)]
pub enum Definition<T, Expr, ConstantRecordTag, PackageName> { pub enum Definition<T, Expr, ConstantRecordTag, PackageName> {
Fn(Function<T, Expr>), Fn(Function<T, Expr>),

1181
crates/lang/src/builder.rs Normal file

File diff suppressed because it is too large Load Diff

View File

@ -5,6 +5,7 @@ use std::sync::{
pub mod air; pub mod air;
pub mod ast; pub mod ast;
pub mod builder;
pub mod builtins; pub mod builtins;
pub mod expr; pub mod expr;
pub mod format; pub mod format;

View File

@ -135,6 +135,14 @@ impl Type {
} }
} }
pub fn is_option(&self) -> bool {
match self {
Self::App { module, name, .. } if "Option" == name && module.is_empty() => true,
Self::Var { tipo } => tipo.borrow().is_option(),
_ => false,
}
}
pub fn is_map(&self) -> bool { pub fn is_map(&self) -> bool {
match self { match self {
Self::App { Self::App {
@ -143,7 +151,7 @@ impl Type {
if let Type::Tuple { elems } = &*args[0] { if let Type::Tuple { elems } = &*args[0] {
elems.len() == 2 elems.len() == 2
} else if let Type::Var { tipo } = &*args[0] { } else if let Type::Var { tipo } = &*args[0] {
matches!(tipo.borrow().get_uplc_type(), UplcType::Pair(_, _)) matches!(tipo.borrow().get_uplc_type(), Some(UplcType::Pair(_, _)))
} else { } else {
false false
} }
@ -157,7 +165,42 @@ impl Type {
matches!(self, Self::Tuple { .. }) matches!(self, Self::Tuple { .. })
} }
pub fn get_inner_type(&self) -> Vec<Arc<Type>> { pub fn is_generic(&self) -> bool {
match self {
Type::App { args, .. } => {
let mut is_a_generic = false;
for arg in args {
is_a_generic = is_a_generic || arg.is_generic();
}
is_a_generic
}
Type::Var { tipo } => tipo.borrow().is_generic(),
Type::Tuple { elems } => {
let mut is_a_generic = false;
for elem in elems {
is_a_generic = is_a_generic || elem.is_generic();
}
is_a_generic
}
Type::Fn { args, .. } => {
let mut is_a_generic = false;
for arg in args {
is_a_generic = is_a_generic || arg.is_generic();
}
is_a_generic
}
}
}
pub fn get_generic(&self) -> Option<u64> {
match self {
Type::Var { tipo } => tipo.borrow().get_generic(),
_ => None,
}
}
pub fn get_inner_types(&self) -> Vec<Arc<Type>> {
if self.is_list() { if self.is_list() {
match self { match self {
Self::App { args, .. } => args.clone(), Self::App { args, .. } => args.clone(),
@ -169,6 +212,13 @@ impl Type {
Self::Tuple { elems } => elems.to_vec(), Self::Tuple { elems } => elems.to_vec(),
_ => vec![], _ => vec![],
} }
} else if matches!(self.get_uplc_type(), UplcType::Data) {
match self {
Type::App { args, .. } => args.clone(),
Type::Fn { args, .. } => args.clone(),
Type::Var { tipo } => tipo.borrow().get_inner_type(),
_ => unreachable!(),
}
} else { } else {
vec![] vec![]
} }
@ -374,6 +424,13 @@ impl TypeVar {
} }
} }
pub fn is_option(&self) -> bool {
match self {
Self::Link { tipo } => tipo.is_option(),
_ => false,
}
}
pub fn is_map(&self) -> bool { pub fn is_map(&self) -> bool {
match self { match self {
Self::Link { tipo } => tipo.is_map(), Self::Link { tipo } => tipo.is_map(),
@ -381,17 +438,33 @@ impl TypeVar {
} }
} }
pub fn is_generic(&self) -> bool {
match self {
TypeVar::Generic { .. } => true,
TypeVar::Link { tipo } => tipo.is_generic(),
_ => false,
}
}
pub fn get_generic(&self) -> Option<u64> {
match self {
TypeVar::Generic { id } => Some(*id),
TypeVar::Link { tipo } => tipo.get_generic(),
_ => None,
}
}
pub fn get_inner_type(&self) -> Vec<Arc<Type>> { pub fn get_inner_type(&self) -> Vec<Arc<Type>> {
match self { match self {
Self::Link { tipo } => tipo.get_inner_type(), Self::Link { tipo } => tipo.get_inner_types(),
_ => vec![], _ => vec![],
} }
} }
pub fn get_uplc_type(&self) -> UplcType { pub fn get_uplc_type(&self) -> Option<UplcType> {
match self { match self {
Self::Link { tipo } => tipo.get_uplc_type(), Self::Link { tipo } => Some(tipo.get_uplc_type()),
_ => unreachable!(), _ => None,
} }
} }
} }

File diff suppressed because it is too large Load Diff

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::{ use std::{
fmt::{Debug, Display}, fmt::{Debug, Display},
io, io,
path::{Path, PathBuf}, path::{Path, PathBuf},
}; };
use uplc::machine::cost_model::ExBudget;
use aiken_lang::{ast::Span, parser::error::ParseError, tipo};
use miette::{
Diagnostic, EyreContext, LabeledSpan, MietteHandlerOpts, NamedSource, RgbColors, SourceCode,
};
#[allow(dead_code)] #[allow(dead_code)]
#[derive(thiserror::Error)] #[derive(thiserror::Error)]
@ -28,7 +33,7 @@ pub enum Error {
#[error(transparent)] #[error(transparent)]
StandardIo(#[from] io::Error), StandardIo(#[from] io::Error),
#[error("Syclical module imports")] #[error("Cyclical module imports")]
ImportCycle { modules: Vec<String> }, ImportCycle { modules: Vec<String> },
/// Useful for returning many [`Error::Parse`] at once /// Useful for returning many [`Error::Parse`] at once
@ -73,6 +78,15 @@ pub enum Error {
src: String, src: String,
named: NamedSource, 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 { impl Error {
@ -148,6 +162,7 @@ impl Error {
Error::Type { path, .. } => Some(path.to_path_buf()), Error::Type { path, .. } => Some(path.to_path_buf()),
Error::ValidatorMustReturnBool { path, .. } => Some(path.to_path_buf()), Error::ValidatorMustReturnBool { path, .. } => Some(path.to_path_buf()),
Error::WrongValidatorArity { 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::Type { src, .. } => Some(src.to_string()),
Error::ValidatorMustReturnBool { src, .. } => Some(src.to_string()), Error::ValidatorMustReturnBool { src, .. } => Some(src.to_string()),
Error::WrongValidatorArity { 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::Format { .. } => None,
Error::ValidatorMustReturnBool { .. } => Some(Box::new("aiken::scripts")), Error::ValidatorMustReturnBool { .. } => Some(Box::new("aiken::scripts")),
Error::WrongValidatorArity { .. } => Some(Box::new("aiken::validators")), 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::Format { .. } => None,
Error::ValidatorMustReturnBool { .. } => Some(Box::new("Try annotating the validator's return type with Bool")), 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::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( Error::WrongValidatorArity { location, .. } => Some(Box::new(
vec![LabeledSpan::new_with_span(None, *location)].into_iter(), vec![LabeledSpan::new_with_span(None, *location)].into_iter(),
)), )),
Error::TestFailure { .. } => None,
} }
} }
@ -259,6 +305,7 @@ impl Diagnostic for Error {
Error::Format { .. } => None, Error::Format { .. } => None,
Error::ValidatorMustReturnBool { named, .. } => Some(named), Error::ValidatorMustReturnBool { named, .. } => Some(named),
Error::WrongValidatorArity { 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 config;
pub mod error; pub mod error;
pub mod format; pub mod format;
pub mod module; pub mod module;
pub mod options; pub mod options;
pub mod pretty;
pub mod script; pub mod script;
pub mod telemetry; pub mod telemetry;
use aiken_lang::{ use aiken_lang::{
ast::{Definition, Function, ModuleKind, TypedFunction}, ast::{
builtins, Annotation, DataType, Definition, Function, ModuleKind, RecordConstructor,
RecordConstructorArg, Span, TypedDataType, TypedDefinition, TypedFunction,
},
builder::{DataTypeKey, FunctionAccessKey},
builtins::{self, generic_var},
tipo::TypeInfo, tipo::TypeInfo,
uplc::{CodeGenerator, DataTypeKey, FunctionAccessKey}, uplc::CodeGenerator,
IdGenerator, IdGenerator,
}; };
use miette::NamedSource; use miette::NamedSource;
@ -26,11 +25,16 @@ use pallas::{
ledger::{addresses::Address, primitives::babbage}, ledger::{addresses::Address, primitives::babbage},
}; };
use pallas_traverse::ComputeHash; use pallas_traverse::ComputeHash;
use script::Script; use script::{EvalHint, EvalInfo, Script};
use serde_json::json; use serde_json::json;
use telemetry::{EventListener, TestInfo}; use std::{
collections::HashMap,
fs,
path::{Path, PathBuf},
};
use telemetry::EventListener;
use uplc::{ use uplc::{
ast::{DeBruijn, Program}, ast::{Constant, DeBruijn, Program, Term},
machine::cost_model::ExBudget, machine::cost_model::ExBudget,
}; };
@ -101,12 +105,20 @@ where
self.compile(options) 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 { let options = Options {
code_gen_mode: if skip_tests { code_gen_mode: if skip_tests {
CodeGenMode::NoOp CodeGenMode::NoOp
} else { } else {
CodeGenMode::Test(match_tests) CodeGenMode::Test {
match_tests,
verbose,
}
}, },
}; };
@ -140,19 +152,47 @@ where
self.event_listener.handle_event(Event::GeneratingUPLC { self.event_listener.handle_event(Event::GeneratingUPLC {
output_path: self.output_path(), output_path: self.output_path(),
}); });
let programs = self.code_gen(validators, &checked_modules)?; let programs = self.code_gen(validators, &checked_modules)?;
self.write_build_outputs(programs, uplc_dump)?; self.write_build_outputs(programs, uplc_dump)?;
Ok(())
} }
CodeGenMode::Test(match_tests) => { CodeGenMode::Test {
let tests = self.test_gen(&checked_modules)?; match_tests,
self.run_tests(tests, match_tests); verbose,
} } => {
CodeGenMode::NoOp => (), 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> { fn read_source_files(&mut self) -> Result<(), Error> {
@ -290,7 +330,7 @@ where
fn validate_validators( fn validate_validators(
&self, &self,
checked_modules: &mut CheckedModules, checked_modules: &mut CheckedModules,
) -> Result<Vec<(String, TypedFunction)>, Error> { ) -> Result<Vec<(PathBuf, String, TypedFunction)>, Error> {
let mut errors = Vec::new(); let mut errors = Vec::new();
let mut validators = Vec::new(); let mut validators = Vec::new();
let mut indices_to_remove = 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); indices_to_remove.push(index);
} }
} }
@ -364,7 +408,7 @@ where
fn code_gen( fn code_gen(
&mut self, &mut self,
validators: Vec<(String, TypedFunction)>, validators: Vec<(PathBuf, String, TypedFunction)>,
checked_modules: &CheckedModules, checked_modules: &CheckedModules,
) -> Result<Vec<Script>, Error> { ) -> Result<Vec<Script>, Error> {
let mut programs = Vec::new(); let mut programs = Vec::new();
@ -374,6 +418,16 @@ where
let mut imports = HashMap::new(); let mut imports = HashMap::new();
let mut constants = 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 module in checked_modules.values() {
for def in module.ast.definitions() { for def in module.ast.definitions() {
match def { match def {
@ -382,6 +436,7 @@ where
FunctionAccessKey { FunctionAccessKey {
module_name: module.name.clone(), module_name: module.name.clone(),
function_name: func.name.clone(), function_name: func.name.clone(),
variant_name: String::new(),
}, },
func, func,
); );
@ -409,7 +464,7 @@ where
} }
} }
for (module_name, func_def) in validators { for (input_path, module_name, func_def) in validators {
let Function { let Function {
arguments, arguments,
name, name,
@ -426,9 +481,15 @@ where
&self.module_types, &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); programs.push(script);
} }
@ -437,7 +498,11 @@ where
} }
// TODO: revisit ownership and lifetimes of data in this function // 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 programs = Vec::new();
let mut functions = HashMap::new(); let mut functions = HashMap::new();
let mut type_aliases = HashMap::new(); let mut type_aliases = HashMap::new();
@ -445,8 +510,18 @@ where
let mut imports = HashMap::new(); let mut imports = HashMap::new();
let mut constants = 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 indices_to_remove = Vec::new();
let mut tests = Vec::new(); let mut scripts = Vec::new();
for module in checked_modules.values() { for module in checked_modules.values() {
for (_index, def) in module.ast.definitions().enumerate() { for (_index, def) in module.ast.definitions().enumerate() {
@ -456,12 +531,18 @@ where
FunctionAccessKey { FunctionAccessKey {
module_name: module.name.clone(), module_name: module.name.clone(),
function_name: func.name.clone(), function_name: func.name.clone(),
variant_name: String::new(),
}, },
func, func,
); );
if should_collect(def) {
scripts.push((module.input_path.clone(), module.name.clone(), func));
}
} }
Definition::Test(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); // indices_to_remove.push(index);
} }
Definition::TypeAlias(ta) => { 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 { let Function {
arguments, arguments,
name, name,
@ -507,9 +588,34 @@ where
&self.module_types, &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); programs.push(script);
} }
@ -517,7 +623,7 @@ where
Ok(programs) 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 // TODO: in the future we probably just want to be able to
// tell the machine to not explode on budget consumption. // tell the machine to not explode on budget consumption.
let initial_budget = ExBudget { let initial_budget = ExBudget {
@ -525,43 +631,41 @@ where
cpu: i64::MAX, cpu: i64::MAX,
}; };
if !tests.is_empty() {
self.event_listener.handle_event(Event::RunningTests);
}
let mut results = Vec::new(); let mut results = Vec::new();
for test in tests { for script in scripts {
let path = format!("{}{}", test.module, test.name); 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; continue;
} }
match test.program.eval(initial_budget) { match script.program.eval(initial_budget) {
(Ok(..), remaining_budget, _) => { (Ok(result), remaining_budget, _) => {
let test_info = TestInfo { let eval_info = EvalInfo {
is_passing: true, success: result != Term::Error
test, && result != Term::Constant(Constant::Bool(false)),
script,
spent_budget: initial_budget - remaining_budget, spent_budget: initial_budget - remaining_budget,
output: Some(result),
}; };
results.push(test_info); results.push(eval_info);
} }
(Err(_), remaining_budget, _) => { (Err(..), remaining_budget, _) => {
let test_info = TestInfo { let eval_info = EvalInfo {
is_passing: false, success: false,
test, script,
spent_budget: initial_budget - remaining_budget, spent_budget: initial_budget - remaining_budget,
output: None,
}; };
results.push(test_info); results.push(eval_info);
} }
} }
} }
self.event_listener results
.handle_event(Event::FinishedTests { tests: results });
} }
fn output_path(&self) -> PathBuf { 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"), .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 { pub enum CodeGenMode {
Test(Option<String>), Test {
match_tests: Option<String>,
verbose: bool,
},
Build(bool), Build(bool),
NoOp, 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}; use uplc::ast::{NamedDeBruijn, Program};
#[derive(Debug)] #[derive(Debug)]
pub struct Script { pub struct Script {
pub input_path: PathBuf,
pub module: String, pub module: String,
pub name: String, pub name: String,
pub program: Program<NamedDeBruijn>, pub program: Program<NamedDeBruijn>,
pub evaluation_hint: Option<EvalHint>,
} }
impl Script { 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 { Script {
input_path,
module, module,
name, name,
program, 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 std::path::PathBuf;
use uplc::machine::cost_model::ExBudget;
pub trait EventListener: std::fmt::Debug { pub trait EventListener: std::fmt::Debug {
fn handle_event(&self, event: Event); fn handle_event(&self, event: Event);
@ -17,14 +16,11 @@ pub enum Event {
GeneratingUPLC { GeneratingUPLC {
output_path: PathBuf, output_path: PathBuf,
}, },
EvaluatingFunction {
results: Vec<EvalInfo>,
},
RunningTests, RunningTests,
FinishedTests { FinishedTests {
tests: Vec<TestInfo>, tests: Vec<EvalInfo>,
}, },
} }
pub struct TestInfo {
pub is_passing: bool,
pub test: Script,
pub spent_budget: ExBudget,
}

View File

@ -0,0 +1,2 @@
name = "acceptance_test_001"
version = "0.0.0"

View File

@ -0,0 +1,14 @@
pub fn length(xs: List<a>) -> Int {
when xs is {
[] -> 0
[_, ..rest] -> 1 + length(rest)
}
}
test length_1() {
length([1, 2, 3]) == 3
}
test length_2() {
length([]) == 0
}

View File

@ -0,0 +1,2 @@
name = "acceptance_test_002"
version = "0.0.0"

View File

@ -0,0 +1,11 @@
pub fn repeat(x: a, n: Int) -> List<a> {
if n <= 0 {
[]
} else {
[x, ..repeat(x, n - 1)]
}
}
test repeat_1() {
repeat("aiken", 2) == ["aiken", "aiken"]
}

View File

@ -0,0 +1,2 @@
name = "acceptance_test_003"
version = "0.0.0"

View File

@ -0,0 +1,14 @@
pub fn foldr(xs: List<a>, f: fn(a, b) -> b, zero: b) -> b {
when xs is {
[] -> zero
[x, ..rest] -> f(x, foldr(rest, f, zero))
}
}
pub fn concat(left: List<a>, right: List<a>) -> List<a> {
foldr(left, fn(x, xs) { [x, ..xs] }, right)
}
test concat_1() {
concat([1, 2, 3], [4, 5, 6]) == [1, 2, 3, 4, 5, 6]
}

View File

@ -0,0 +1,2 @@
name = "acceptance_test_004"
version = "0.0.0"

View File

@ -0,0 +1,18 @@
pub fn foldr(xs: List<a>, f: fn(a, b) -> b, zero: b) -> b {
when xs is {
[] -> zero
[x, ..rest] -> f(x, foldr(rest, f, zero))
}
}
pub fn prepend(x: a, xs: List<a>) -> List<a> {
[x, ..xs]
}
pub fn concat(left: List<a>, right: List<a>) -> List<a> {
foldr(left, prepend, right)
}
test concat_1() {
concat([1, 2, 3], [4, 5, 6]) == [1, 2, 3, 4, 5, 6]
}

View File

@ -0,0 +1,2 @@
name = "acceptance_test_005"
version = "0.0.0"

View File

@ -0,0 +1,16 @@
use aiken/builtin.{head_list}
pub fn head(xs: List<a>) -> Option<a> {
when xs is {
[] -> None
_ -> Some(head_list(xs))
}
}
test head_1() {
head([1, 2, 3]) == Some(1)
}
test head_2() {
head([]) == None
}

View File

@ -0,0 +1,2 @@
name = "acceptance_test_006"
version = "0.0.0"

View File

@ -0,0 +1,3 @@
test foo() {
#(1, []) == #(1, [])
}

View File

@ -0,0 +1,2 @@
name = "acceptance_test_007"
version = "0.0.0"

View File

@ -0,0 +1,9 @@
pub fn unzip(xs: List<#(a, b)>) -> #(List<a>, List<b>) {
when xs is {
[] -> #([], [])
[#(a, b), ..rest] -> {
let #(a_tail, b_tail) = unzip(rest)
#([a, ..a_tail], [b, ..b_tail])
}
}
}

View File

@ -0,0 +1,2 @@
name = "acceptance_test_008"
version = "0.0.0"

View File

@ -0,0 +1,21 @@
use aiken/builtin
pub fn is_empty(bytes: ByteArray) -> Bool {
builtin.length_of_bytearray(bytes) == 0
}
test is_empty_1() {
is_empty(#[]) == True
}
test is_empty_1_alt() {
is_empty(#[])
}
test is_empty_2() {
is_empty(#[1]) == False
}
test is_empty_2_alt() {
!is_empty(#[1])
}

View File

@ -0,0 +1,2 @@
name = "acceptance_test_009"
version = "0.0.0"

View File

@ -0,0 +1,9 @@
use aiken/builtin.{length_of_bytearray}
pub fn is_empty(bytes: ByteArray) -> Bool {
length_of_bytearray(bytes) == 0
}
test is_empty_1() {
is_empty(#[]) == True
}

View File

@ -0,0 +1,2 @@
name = "acceptance_test_010"
version = "0.0.0"

View File

@ -0,0 +1,14 @@
pub fn map(opt: Option<a>, f: fn(a) -> b) -> Option<b> {
when opt is {
None -> None
Some(a) -> Some(f(a))
}
}
fn add_one(n: Int) -> Int {
n + 1
}
test map_1() {
map(None, add_one) == None
}

View File

@ -0,0 +1,2 @@
name = "acceptance_test_011"
version = "0.0.0"

View File

@ -0,0 +1,10 @@
pub fn map(xs: List<a>, f: fn(a) -> result) -> List<result> {
when xs is {
[] -> []
[x, ..rest] -> [f(x), ..map(rest, f)]
}
}
test map_1() {
map([], fn(n) { n + 1 }) == []
}

View File

@ -0,0 +1,2 @@
name = "acceptance_test_012"
version = "0.0.0"

View File

@ -0,0 +1,18 @@
use aiken/builtin
pub fn filter(xs: List<a>, f: fn(a) -> Bool) -> List<a> {
when xs is {
[] -> []
[x, ..rest] ->
if f(x) {
[x, ..filter(rest, f)]
} else {
filter(rest, f)
}
}
}
test filter_1() {
filter([1,
2, 3, 4, 5, 6], fn(x) { builtin.mod_integer(x, 2) == 0 }) == [2, 4, 6]
}

View File

@ -0,0 +1,2 @@
name = "acceptance_test_013"
version = "0.0.0"

View File

@ -0,0 +1,13 @@
pub fn unzip(xs: List<#(a, b)>) -> #(List<a>, List<b>) {
when xs is {
[] -> #([], [])
[#(a, b), ..rest] -> {
let #(a_tail, b_tail) = unzip(rest)
#([a, ..a_tail], [b, ..b_tail])
}
}
}
test unzip_1() {
unzip([#(1, "a"), #(2, "b")]) == #([1, 2], ["a", "b"])
}

View File

@ -0,0 +1,2 @@
name = "acceptance_test_014"
version = "0.0.0"

View File

@ -0,0 +1,7 @@
// NOTE:
// Somehow, the left-hand evaluates to: [#00, #20, #21]
// whereas the right-hand evaluates to: [#21, #20, #00]
//
test foo() {
[0 - 2, 0 - 1, 0] == [-2, -1, 0]
}

View File

@ -0,0 +1,2 @@
name = "acceptance_test_015"
version = "0.0.0"

View File

@ -0,0 +1,11 @@
pub opaque type Map<key, value> {
inner: List<#(key, value)>,
}
pub fn new() {
Map { inner: [] }
}
test new_1() {
new() == Map { inner: [] }
}

View File

@ -0,0 +1,5 @@
all:
@for t in $(shell find . -regex ".*[0-9]\{3\}" -type d | sort); do \
cargo run --quiet -- check -d $${t}; \
echo ""; \
done