Add 'eval' command to evaluate target aiken function

Pretty useful for debbugging. Though, on second-thoughts, this is
  something we may want to review later and maybe have that done by
  default for tests.

  At the moment, we expects tests to unify to `bool`, and treat `false`
  values as failing tests. Yet, on failures, this gives little
  information about what's wrong with the test.

  It'd be nice to either have better way to assert in tests, or, to
  simply accept non-bool tests, and show whatever the test evaluates
  to as a debug output.
This commit is contained in:
KtorZ 2022-12-14 00:31:14 +01:00
parent 95df5f9137
commit b6962ba9d3
No known key found for this signature in database
GPG Key ID: 33173CB6F77F4277
8 changed files with 176 additions and 70 deletions

View File

@ -0,0 +1,27 @@
use aiken_project::options::{CodeGenMode, Options};
use std::path::PathBuf;
#[derive(clap::Args)]
/// Evaluate a chosen function with no argument.
pub struct Args {
/// Path to project
#[clap(short, long)]
directory: Option<PathBuf>,
/// Evaluate the given function
#[clap(short, long)]
function_name: String,
}
pub fn exec(
Args {
directory,
function_name,
}: Args,
) -> miette::Result<()> {
crate::with_project(directory, |p| {
p.compile(Options {
code_gen_mode: CodeGenMode::Eval(function_name.clone()),
})
})
}

View File

@ -1,6 +1,7 @@
pub mod build;
pub mod check;
pub mod error;
pub mod eval;
pub mod fmt;
pub mod lsp;
pub mod new;

View File

@ -2,7 +2,7 @@ use std::{env, path::PathBuf};
use aiken_project::{
config::Config,
telemetry::{self, TestInfo},
telemetry::{self, EvalInfo},
Project,
};
use miette::IntoDiagnostic;
@ -76,37 +76,30 @@ impl telemetry::EventListener for Terminal {
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 => {
println!("{}\n", "...Running tests".bold().purple());
}
telemetry::Event::FinishedTests { tests } => {
let (max_mem, max_cpu) = tests.iter().fold(
(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_cpu) = find_max_execution_units(&tests);
let max_mem = max_mem.to_string().len() as i32;
let max_cpu = max_cpu.to_string().len() as i32;
for test_info in &tests {
println!("{}", fmt_test(test_info, max_mem, max_cpu))
for eval_info in &tests {
println!("{}", fmt_test(eval_info, max_mem, max_cpu))
}
let (n_passed, n_failed) =
tests
.iter()
.fold((0, 0), |(n_passed, n_failed), test_info| {
if test_info.is_passing {
if test_info.success {
(n_passed + 1, n_failed)
} else {
(n_passed, n_failed + 1)
@ -128,26 +121,72 @@ impl telemetry::EventListener for Terminal {
}
}
fn fmt_test(test_info: &TestInfo, max_mem: i32, max_cpu: i32) -> String {
let TestInfo {
is_passing,
test,
fn fmt_test(eval_info: &EvalInfo, max_mem: i32, max_cpu: i32) -> String {
let EvalInfo {
success,
script,
spent_budget,
} = test_info;
..
} = eval_info;
let ExBudget { mem, cpu } = spent_budget;
format!(
" [{}] [mem: {}, cpu: {}] {}::{}",
if *is_passing {
if *success {
"PASS".bold().green().to_string()
} else {
"FAIL".bold().red().to_string()
},
pad_left(mem.to_string(), max_mem, " "),
pad_left(cpu.to_string(), max_cpu, " "),
test.module.blue(),
test.name.bright_blue()
script.module.blue(),
script.name.bright_blue()
)
}
fn fmt_eval(eval_info: &EvalInfo, max_mem: i32, max_cpu: i32) -> String {
let EvalInfo {
output,
script,
spent_budget,
..
} = eval_info;
let ExBudget { mem, cpu } = spent_budget;
format!(
" {}::{} [mem: {}, cpu: {}]\n\n ╰─▶ {}",
script.module.blue(),
script.name.bright_blue(),
pad_left(mem.to_string(), max_mem, " "),
pad_left(cpu.to_string(), max_cpu, " "),
output
.as_ref()
.map(|x| format!("{}", x))
.unwrap_or("Error.".to_string()),
)
}
fn find_max_execution_units(xs: &Vec<EvalInfo>) -> (i32, i32) {
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() as i32,
max_cpu.to_string().len() as i32,
)
}

View File

@ -1,4 +1,4 @@
use aiken::cmd::{build, check, fmt, lsp, new, tx, uplc};
use aiken::cmd::{build, check, eval, fmt, lsp, new, tx, uplc};
use clap::Parser;
/// Aiken: a smart-contract language and toolchain for Cardano
@ -11,6 +11,7 @@ pub enum Cmd {
Fmt(fmt::Args),
Build(build::Args),
Check(check::Args),
Eval(eval::Args),
#[clap(hide = true)]
Lsp(lsp::Args),
@ -35,6 +36,7 @@ fn main() -> miette::Result<()> {
Cmd::Fmt(args) => fmt::exec(args),
Cmd::Build(args) => build::exec(args),
Cmd::Check(args) => check::exec(args),
Cmd::Eval(args) => eval::exec(args),
Cmd::Lsp(args) => lsp::exec(args),
Cmd::Tx(sub_cmd) => tx::exec(sub_cmd),
Cmd::Uplc(sub_cmd) => uplc::exec(sub_cmd),

View File

@ -96,7 +96,12 @@ impl<'a> CodeGenerator<'a> {
}
}
pub fn generate(&mut self, body: TypedExpr, arguments: Vec<TypedArg>) -> Program<Name> {
pub fn generate(
&mut self,
body: TypedExpr,
arguments: Vec<TypedArg>,
wrap_as_validator: bool,
) -> Program<Name> {
let mut ir_stack = vec![];
let scope = vec![self.id_gen.next()];
@ -113,7 +118,11 @@ impl<'a> CodeGenerator<'a> {
}
// Wrap the validator body if ifThenElse term unit error
term = builder::final_wrapper(term);
term = if wrap_as_validator {
builder::final_wrapper(term)
} else {
term
};
for arg in arguments.iter().rev() {
term = Term::Lambda {

View File

@ -13,7 +13,7 @@ pub mod script;
pub mod telemetry;
use aiken_lang::{
ast::{Definition, Function, ModuleKind, TypedFunction},
ast::{Definition, Function, ModuleKind, TypedDefinition, TypedFunction},
builtins,
tipo::TypeInfo,
uplc::{CodeGenerator, DataTypeKey, FunctionAccessKey},
@ -28,9 +28,9 @@ use pallas::{
use pallas_traverse::ComputeHash;
use script::Script;
use serde_json::json;
use telemetry::{EventListener, TestInfo};
use telemetry::{EvalInfo, EventListener};
use uplc::{
ast::{DeBruijn, Program},
ast::{Constant, DeBruijn, Program, Term},
machine::cost_model::ExBudget,
};
@ -146,8 +146,25 @@ where
self.write_build_outputs(programs, uplc_dump)?;
}
CodeGenMode::Test(match_tests) => {
let tests = self.test_gen(&checked_modules)?;
self.run_tests(tests, match_tests);
let tests = self.scripts_gen(&checked_modules, |def| match def {
Definition::Test(..) => true,
_ => false,
})?;
if !tests.is_empty() {
self.event_listener.handle_event(Event::RunningTests);
}
let results = self.eval_scripts(tests, match_tests);
self.event_listener
.handle_event(Event::FinishedTests { tests: results });
}
CodeGenMode::Eval(func_name) => {
let scripts = self.scripts_gen(&checked_modules, |def| match def {
Definition::Fn(..) => true,
_ => false,
})?;
let results = self.eval_scripts(scripts, Some(func_name));
self.event_listener
.handle_event(Event::EvaluatingFunction { results });
}
CodeGenMode::NoOp => (),
}
@ -427,7 +444,7 @@ 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());
@ -438,7 +455,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 scripts_gen(
&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();
@ -447,7 +468,7 @@ where
let mut constants = HashMap::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 (_index, def) in module.ast.definitions().enumerate() {
@ -461,9 +482,14 @@ where
},
func,
);
if should_collect(def) {
scripts.push((module.name.clone(), func));
}
}
Definition::Test(func) => {
tests.push((module.name.clone(), func));
if should_collect(def) {
scripts.push((module.name.clone(), func));
}
// indices_to_remove.push(index);
}
Definition::TypeAlias(ta) => {
@ -492,7 +518,7 @@ where
// }
}
for (module_name, func_def) in tests {
for (module_name, func_def) in scripts {
let Function {
arguments,
name,
@ -509,7 +535,7 @@ where
&self.module_types,
);
let program = generator.generate(body.clone(), arguments.clone());
let program = generator.generate(body.clone(), arguments.clone(), false);
let script = Script::new(module_name, name.to_string(), program.try_into().unwrap());
@ -519,7 +545,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 {
@ -527,45 +553,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(e), remaining_budget, _) => {
println!("ERROR:\n{}", e);
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 {

View File

@ -4,6 +4,7 @@ pub struct Options {
pub enum CodeGenMode {
Test(Option<String>),
Eval(String),
Build(bool),
NoOp,
}

View File

@ -1,5 +1,6 @@
use crate::script::Script;
use std::path::PathBuf;
use uplc::ast::{NamedDeBruijn, Term};
use uplc::machine::cost_model::ExBudget;
pub trait EventListener: std::fmt::Debug {
@ -17,14 +18,18 @@ 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 struct EvalInfo {
pub success: bool,
pub script: Script,
pub spent_budget: ExBudget,
pub output: Option<Term<NamedDeBruijn>>,
}