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:
parent
95df5f9137
commit
b6962ba9d3
|
@ -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()),
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
|
@ -1,6 +1,7 @@
|
||||||
pub mod build;
|
pub mod build;
|
||||||
pub mod check;
|
pub mod check;
|
||||||
pub mod error;
|
pub mod error;
|
||||||
|
pub mod eval;
|
||||||
pub mod fmt;
|
pub mod fmt;
|
||||||
pub mod lsp;
|
pub mod lsp;
|
||||||
pub mod new;
|
pub mod new;
|
||||||
|
|
|
@ -2,7 +2,7 @@ use std::{env, path::PathBuf};
|
||||||
|
|
||||||
use aiken_project::{
|
use aiken_project::{
|
||||||
config::Config,
|
config::Config,
|
||||||
telemetry::{self, TestInfo},
|
telemetry::{self, EvalInfo},
|
||||||
Project,
|
Project,
|
||||||
};
|
};
|
||||||
use miette::IntoDiagnostic;
|
use miette::IntoDiagnostic;
|
||||||
|
@ -76,37 +76,30 @@ 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 eval_info in &tests {
|
||||||
let max_cpu = max_cpu.to_string().len() as i32;
|
println!("{}", fmt_test(eval_info, max_mem, max_cpu))
|
||||||
|
|
||||||
for test_info in &tests {
|
|
||||||
println!("{}", fmt_test(test_info, max_mem, max_cpu))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let (n_passed, n_failed) =
|
let (n_passed, n_failed) =
|
||||||
tests
|
tests
|
||||||
.iter()
|
.iter()
|
||||||
.fold((0, 0), |(n_passed, n_failed), test_info| {
|
.fold((0, 0), |(n_passed, n_failed), test_info| {
|
||||||
if test_info.is_passing {
|
if test_info.success {
|
||||||
(n_passed + 1, n_failed)
|
(n_passed + 1, n_failed)
|
||||||
} else {
|
} else {
|
||||||
(n_passed, n_failed + 1)
|
(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 {
|
fn fmt_test(eval_info: &EvalInfo, max_mem: i32, max_cpu: i32) -> 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 ExBudget { mem, cpu } = spent_budget;
|
||||||
|
|
||||||
format!(
|
format!(
|
||||||
" [{}] [mem: {}, cpu: {}] {}::{}",
|
" [{}] [mem: {}, cpu: {}] {}::{}",
|
||||||
if *is_passing {
|
if *success {
|
||||||
"PASS".bold().green().to_string()
|
"PASS".bold().green().to_string()
|
||||||
} else {
|
} else {
|
||||||
"FAIL".bold().red().to_string()
|
"FAIL".bold().red().to_string()
|
||||||
},
|
},
|
||||||
pad_left(mem.to_string(), max_mem, " "),
|
pad_left(mem.to_string(), max_mem, " "),
|
||||||
pad_left(cpu.to_string(), max_cpu, " "),
|
pad_left(cpu.to_string(), max_cpu, " "),
|
||||||
test.module.blue(),
|
script.module.blue(),
|
||||||
test.name.bright_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,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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;
|
use clap::Parser;
|
||||||
|
|
||||||
/// Aiken: a smart-contract language and toolchain for Cardano
|
/// Aiken: a smart-contract language and toolchain for Cardano
|
||||||
|
@ -11,6 +11,7 @@ pub enum Cmd {
|
||||||
Fmt(fmt::Args),
|
Fmt(fmt::Args),
|
||||||
Build(build::Args),
|
Build(build::Args),
|
||||||
Check(check::Args),
|
Check(check::Args),
|
||||||
|
Eval(eval::Args),
|
||||||
|
|
||||||
#[clap(hide = true)]
|
#[clap(hide = true)]
|
||||||
Lsp(lsp::Args),
|
Lsp(lsp::Args),
|
||||||
|
@ -35,6 +36,7 @@ fn main() -> miette::Result<()> {
|
||||||
Cmd::Fmt(args) => fmt::exec(args),
|
Cmd::Fmt(args) => fmt::exec(args),
|
||||||
Cmd::Build(args) => build::exec(args),
|
Cmd::Build(args) => build::exec(args),
|
||||||
Cmd::Check(args) => check::exec(args),
|
Cmd::Check(args) => check::exec(args),
|
||||||
|
Cmd::Eval(args) => eval::exec(args),
|
||||||
Cmd::Lsp(args) => lsp::exec(args),
|
Cmd::Lsp(args) => lsp::exec(args),
|
||||||
Cmd::Tx(sub_cmd) => tx::exec(sub_cmd),
|
Cmd::Tx(sub_cmd) => tx::exec(sub_cmd),
|
||||||
Cmd::Uplc(sub_cmd) => uplc::exec(sub_cmd),
|
Cmd::Uplc(sub_cmd) => uplc::exec(sub_cmd),
|
||||||
|
|
|
@ -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 mut ir_stack = vec![];
|
||||||
let scope = vec![self.id_gen.next()];
|
let scope = vec![self.id_gen.next()];
|
||||||
|
|
||||||
|
@ -113,7 +118,11 @@ impl<'a> CodeGenerator<'a> {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Wrap the validator body if ifThenElse term unit error
|
// 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() {
|
for arg in arguments.iter().rev() {
|
||||||
term = Term::Lambda {
|
term = Term::Lambda {
|
||||||
|
|
|
@ -13,7 +13,7 @@ pub mod script;
|
||||||
pub mod telemetry;
|
pub mod telemetry;
|
||||||
|
|
||||||
use aiken_lang::{
|
use aiken_lang::{
|
||||||
ast::{Definition, Function, ModuleKind, TypedFunction},
|
ast::{Definition, Function, ModuleKind, TypedDefinition, TypedFunction},
|
||||||
builtins,
|
builtins,
|
||||||
tipo::TypeInfo,
|
tipo::TypeInfo,
|
||||||
uplc::{CodeGenerator, DataTypeKey, FunctionAccessKey},
|
uplc::{CodeGenerator, DataTypeKey, FunctionAccessKey},
|
||||||
|
@ -28,9 +28,9 @@ use pallas::{
|
||||||
use pallas_traverse::ComputeHash;
|
use pallas_traverse::ComputeHash;
|
||||||
use script::Script;
|
use script::Script;
|
||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
use telemetry::{EventListener, TestInfo};
|
use telemetry::{EvalInfo, EventListener};
|
||||||
use uplc::{
|
use uplc::{
|
||||||
ast::{DeBruijn, Program},
|
ast::{Constant, DeBruijn, Program, Term},
|
||||||
machine::cost_model::ExBudget,
|
machine::cost_model::ExBudget,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -146,8 +146,25 @@ where
|
||||||
self.write_build_outputs(programs, uplc_dump)?;
|
self.write_build_outputs(programs, uplc_dump)?;
|
||||||
}
|
}
|
||||||
CodeGenMode::Test(match_tests) => {
|
CodeGenMode::Test(match_tests) => {
|
||||||
let tests = self.test_gen(&checked_modules)?;
|
let tests = self.scripts_gen(&checked_modules, |def| match def {
|
||||||
self.run_tests(tests, match_tests);
|
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 => (),
|
CodeGenMode::NoOp => (),
|
||||||
}
|
}
|
||||||
|
@ -427,7 +444,7 @@ 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(module_name, name, program.try_into().unwrap());
|
||||||
|
|
||||||
|
@ -438,7 +455,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 scripts_gen(
|
||||||
|
&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();
|
||||||
|
@ -447,7 +468,7 @@ where
|
||||||
let mut constants = HashMap::new();
|
let mut constants = HashMap::new();
|
||||||
|
|
||||||
// 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() {
|
||||||
|
@ -461,9 +482,14 @@ where
|
||||||
},
|
},
|
||||||
func,
|
func,
|
||||||
);
|
);
|
||||||
|
if should_collect(def) {
|
||||||
|
scripts.push((module.name.clone(), func));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Definition::Test(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);
|
// indices_to_remove.push(index);
|
||||||
}
|
}
|
||||||
Definition::TypeAlias(ta) => {
|
Definition::TypeAlias(ta) => {
|
||||||
|
@ -492,7 +518,7 @@ where
|
||||||
// }
|
// }
|
||||||
}
|
}
|
||||||
|
|
||||||
for (module_name, func_def) in tests {
|
for (module_name, func_def) in scripts {
|
||||||
let Function {
|
let Function {
|
||||||
arguments,
|
arguments,
|
||||||
name,
|
name,
|
||||||
|
@ -509,7 +535,7 @@ where
|
||||||
&self.module_types,
|
&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());
|
let script = Script::new(module_name, name.to_string(), program.try_into().unwrap());
|
||||||
|
|
||||||
|
@ -519,7 +545,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 {
|
||||||
|
@ -527,45 +553,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(e), remaining_budget, _) => {
|
(Err(..), remaining_budget, _) => {
|
||||||
println!("ERROR:\n{}", e);
|
let eval_info = EvalInfo {
|
||||||
|
success: false,
|
||||||
let test_info = TestInfo {
|
script,
|
||||||
is_passing: false,
|
|
||||||
test,
|
|
||||||
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 {
|
||||||
|
|
|
@ -4,6 +4,7 @@ pub struct Options {
|
||||||
|
|
||||||
pub enum CodeGenMode {
|
pub enum CodeGenMode {
|
||||||
Test(Option<String>),
|
Test(Option<String>),
|
||||||
|
Eval(String),
|
||||||
Build(bool),
|
Build(bool),
|
||||||
NoOp,
|
NoOp,
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
use crate::script::Script;
|
use crate::script::Script;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
use uplc::ast::{NamedDeBruijn, Term};
|
||||||
use uplc::machine::cost_model::ExBudget;
|
use uplc::machine::cost_model::ExBudget;
|
||||||
|
|
||||||
pub trait EventListener: std::fmt::Debug {
|
pub trait EventListener: std::fmt::Debug {
|
||||||
|
@ -17,14 +18,18 @@ 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 struct EvalInfo {
|
||||||
pub is_passing: bool,
|
pub success: bool,
|
||||||
pub test: Script,
|
pub script: Script,
|
||||||
pub spent_budget: ExBudget,
|
pub spent_budget: ExBudget,
|
||||||
|
pub output: Option<Term<NamedDeBruijn>>,
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue