From 6d0d938fb9da98ec29f8329b76ebd1a0f015c12b Mon Sep 17 00:00:00 2001 From: KtorZ Date: Fri, 28 Oct 2022 13:48:40 +0200 Subject: [PATCH 1/4] Extra project utilities in their own crate. This was currently in the 'cli' crates, but this code is pretty standalone and need not to be mixed with the rest of the cli logic. Ideally, we want the cli crate to be only a thin wrapper over functionality available from the rest of the lib crates. --- Cargo.lock | 23 +++++++++++++------ crates/cli/Cargo.toml | 15 ++++-------- crates/cli/src/lib.rs | 5 ---- crates/cli/src/main.rs | 2 +- crates/project/Cargo.toml | 20 ++++++++++++++++ crates/project/README.md | 3 +++ crates/{cli => project}/src/config.rs | 0 crates/{cli => project}/src/error.rs | 0 .../src/project.rs => project/src/lib.rs} | 4 ++++ crates/{cli => project}/src/module.rs | 0 10 files changed, 49 insertions(+), 23 deletions(-) create mode 100644 crates/project/Cargo.toml create mode 100644 crates/project/README.md rename crates/{cli => project}/src/config.rs (100%) rename crates/{cli => project}/src/error.rs (100%) rename crates/{cli/src/project.rs => project/src/lib.rs} (99%) rename crates/{cli => project}/src/module.rs (100%) diff --git a/Cargo.lock b/Cargo.lock index 38d31c98..897ed1c4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -61,14 +61,8 @@ dependencies = [ "pallas-crypto", "pallas-primitives", "pallas-traverse", - "petgraph", - "regex", - "serde", - "serde_json", - "thiserror", - "toml", + "project", "uplc", - "walkdir", ] [[package]] @@ -808,6 +802,21 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "project" +version = "0.0.21" +dependencies = [ + "aiken-lang", + "miette", + "petgraph", + "regex", + "serde", + "serde_json", + "thiserror", + "toml", + "walkdir", +] + [[package]] name = "proptest" version = "1.0.0" diff --git a/crates/cli/Cargo.toml b/crates/cli/Cargo.toml index 19e34dc7..b566e94b 100644 --- a/crates/cli/Cargo.toml +++ b/crates/cli/Cargo.toml @@ -12,19 +12,14 @@ authors = ["Lucas Rosa ", "Kasey White "] anyhow = "1.0.57" clap = { version = "3.1.14", features = ["derive"] } hex = "0.4.3" +ignore = "0.4.18" +miette = { version = "5.3.0", features = ["fancy"] } pallas-addresses = "0.14.0-alpha.3" pallas-codec = "0.14.0-alpha.3" pallas-crypto = "0.14.0-alpha.3" pallas-primitives = "0.14.0-alpha.3" pallas-traverse = "0.14.0-alpha.3" -serde = { version = "1.0.144", features = ["derive"] } -serde_json = "1.0.85" -uplc = { path = '../uplc', version = "0.0.21" } + aiken-lang = { path = "../lang", version = "0.0.20" } -toml = "0.5.9" -walkdir = "2.3.2" -ignore = "0.4.18" -regex = "1.6.0" -miette = { version = "5.3.0", features = ["fancy"] } -thiserror = "1.0.37" -petgraph = "0.6.2" +project = { path = '../project', version = "0.0.21" } +uplc = { path = '../uplc', version = "0.0.21" } diff --git a/crates/cli/src/lib.rs b/crates/cli/src/lib.rs index b2a46373..362113c6 100644 --- a/crates/cli/src/lib.rs +++ b/crates/cli/src/lib.rs @@ -1,7 +1,2 @@ -pub mod config; -pub mod error; -pub mod module; -pub mod project; - pub use aiken_lang; pub use uplc; diff --git a/crates/cli/src/main.rs b/crates/cli/src/main.rs index 6faa2f2b..dc28d59e 100644 --- a/crates/cli/src/main.rs +++ b/crates/cli/src/main.rs @@ -16,7 +16,7 @@ use uplc::{ }, }; -use aiken::{config::Config, project::Project}; +use project::{config::Config, Project}; mod args; diff --git a/crates/project/Cargo.toml b/crates/project/Cargo.toml new file mode 100644 index 00000000..2d37e918 --- /dev/null +++ b/crates/project/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "project" +description = "Aiken project utilities" +version = "0.0.21" +edition = "2021" +repository = "https://github.com/txpipe/aiken/crates/project" +homepage = "https://github.com/txpipe/aiken" +license = "Apache-2.0" +authors = ["Lucas Rosa ", "Kasey White "] + +[dependencies] +aiken-lang = { path = "../lang", version = "0.0.20" } +miette = { version = "5.3.0", features = ["fancy"] } +petgraph = "0.6.2" +regex = "1.6.0" +serde = { version = "1.0.144", features = ["derive"] } +serde_json = "1.0.85" +thiserror = "1.0.37" +toml = "0.5.9" +walkdir = "2.3.2" diff --git a/crates/project/README.md b/crates/project/README.md new file mode 100644 index 00000000..7328ab4c --- /dev/null +++ b/crates/project/README.md @@ -0,0 +1,3 @@ +# Project + +This crate encapsulates the code used to manage Aiken projects. See [crates/cli](../cli) for usage. diff --git a/crates/cli/src/config.rs b/crates/project/src/config.rs similarity index 100% rename from crates/cli/src/config.rs rename to crates/project/src/config.rs diff --git a/crates/cli/src/error.rs b/crates/project/src/error.rs similarity index 100% rename from crates/cli/src/error.rs rename to crates/project/src/error.rs diff --git a/crates/cli/src/project.rs b/crates/project/src/lib.rs similarity index 99% rename from crates/cli/src/project.rs rename to crates/project/src/lib.rs index bc5ab999..c7da293f 100644 --- a/crates/cli/src/project.rs +++ b/crates/project/src/lib.rs @@ -4,6 +4,10 @@ use std::{ path::{Path, PathBuf}, }; +pub mod config; +pub mod error; +pub mod module; + use aiken_lang::{ast::ModuleKind, builtins, tipo::TypeInfo, IdGenerator}; use crate::{ diff --git a/crates/cli/src/module.rs b/crates/project/src/module.rs similarity index 100% rename from crates/cli/src/module.rs rename to crates/project/src/module.rs From 9c608ad9f1abd864aada2cd8290479ce91292f1a Mon Sep 17 00:00:00 2001 From: KtorZ Date: Fri, 28 Oct 2022 15:14:14 +0200 Subject: [PATCH 2/4] Refactor cli's crate; split code into a hierarchy of modules. This follows a simple convention: - `main.rs` contains as little as possible and delegates both data-types definitions and command executions to sub-modules. - modules are named after their respective commands. For sub-commands, - Each command module can be in one of two forms: - Either it is a leaf command, and it then contains an `Args` struct that defines the command arguments; and a function `exec` when outlines the execution logic. - Or, it is a group command with multiple sub-commands. In which case the module defines a `Cmd` struct encapsulating all sub-commands; and also an `exec` function which simply dispatches the logic to sub-functions. --- This commit also removes the `dev` command which is currently unused. The rationale being: if it's not there, it's not there. --- crates/cli/src/args.rs | 129 ----------- crates/cli/src/cmd/build.rs | 44 ++++ crates/cli/src/cmd/check.rs | 46 ++++ crates/cli/src/cmd/mod.rs | 5 + crates/cli/src/cmd/new.rs | 20 ++ crates/cli/src/cmd/tx/mod.rs | 15 ++ crates/cli/src/cmd/tx/simulate.rs | 125 ++++++++++ crates/cli/src/cmd/uplc/eval.rs | 75 ++++++ crates/cli/src/cmd/uplc/flat.rs | 83 +++++++ crates/cli/src/cmd/uplc/fmt.rs | 30 +++ crates/cli/src/cmd/uplc/mod.rs | 24 ++ crates/cli/src/cmd/uplc/unflat.rs | 61 +++++ crates/cli/src/lib.rs | 2 - crates/cli/src/main.rs | 374 +++--------------------------- 14 files changed, 556 insertions(+), 477 deletions(-) delete mode 100644 crates/cli/src/args.rs create mode 100644 crates/cli/src/cmd/build.rs create mode 100644 crates/cli/src/cmd/check.rs create mode 100644 crates/cli/src/cmd/mod.rs create mode 100644 crates/cli/src/cmd/new.rs create mode 100644 crates/cli/src/cmd/tx/mod.rs create mode 100644 crates/cli/src/cmd/tx/simulate.rs create mode 100644 crates/cli/src/cmd/uplc/eval.rs create mode 100644 crates/cli/src/cmd/uplc/flat.rs create mode 100644 crates/cli/src/cmd/uplc/fmt.rs create mode 100644 crates/cli/src/cmd/uplc/mod.rs create mode 100644 crates/cli/src/cmd/uplc/unflat.rs delete mode 100644 crates/cli/src/lib.rs diff --git a/crates/cli/src/args.rs b/crates/cli/src/args.rs deleted file mode 100644 index c6bf8bb0..00000000 --- a/crates/cli/src/args.rs +++ /dev/null @@ -1,129 +0,0 @@ -use std::path::PathBuf; - -use clap::{Parser, Subcommand}; - -/// Cardano smart contract toolchain -#[derive(Parser)] -#[clap(version, about, long_about = None)] -#[clap(propagate_version = true)] -pub enum Args { - /// Build an aiken project - Build { - /// Path to project - #[clap(short, long)] - directory: Option, - }, - /// Typecheck a project project - Check { - /// Path to project - #[clap(short, long)] - directory: Option, - }, - /// Start a development server - Dev, - /// Create a new aiken project - New { - /// Project name - name: PathBuf, - }, - /// A subcommand for working with transactions - #[clap(subcommand)] - Tx(TxCommand), - /// A subcommand for working with Untyped Plutus Core - #[clap(subcommand)] - Uplc(UplcCommand), -} - -/// Commands for working with transactions -#[derive(Subcommand)] -pub enum TxCommand { - /// Simulate a transaction by evaluating it's script - Simulate { - /// A file containing cbor hex for a transaction - input: PathBuf, - - /// Toggle whether input is raw cbor or a hex string - #[clap(short, long)] - cbor: bool, - - /// A file containing cbor hex for the raw inputs - raw_inputs: PathBuf, - - /// A file containing cbor hex for the raw outputs - raw_outputs: PathBuf, - - /// Time between each slot - #[clap(short, long, default_value_t = 1000)] - slot_length: u32, - - /// Time of shelley hardfork - #[clap(long, default_value_t = 1596059091000)] - zero_time: u64, - - /// Slot number at the start of the shelley hardfork - #[clap(long, default_value_t = 4492800)] - zero_slot: u64, - }, -} - -/// Commands for working with Untyped Plutus Core -#[derive(Subcommand)] -pub enum UplcCommand { - /// Evaluate an Untyped Plutus Core program - Eval { - script: PathBuf, - - #[clap(short, long)] - flat: bool, - - /// Arguments to pass to the uplc program - args: Vec, - }, - /// Encode textual Untyped Plutus Core to flat bytes - Flat { - /// Textual Untyped Plutus Core file - input: PathBuf, - - /// Output file name - #[clap(short, long)] - out: Option, - - /// Print output instead of saving to file - #[clap(short, long)] - print: bool, - - #[clap(short, long)] - cbor_hex: bool, - }, - /// Format an Untyped Plutus Core program - Fmt { - /// Textual Untyped Plutus Core file - input: PathBuf, - - /// Print output instead of saving to file - #[clap(short, long)] - print: bool, - }, - /// Decode flat bytes to textual Untyped Plutus Core - Unflat { - /// Flat encoded Untyped Plutus Core file - input: PathBuf, - - /// Output file name - #[clap(short, long)] - out: Option, - - /// Print output instead of saving to file - #[clap(short, long)] - print: bool, - - #[clap(short, long)] - cbor_hex: bool, - }, -} - -impl Default for Args { - fn default() -> Self { - Self::parse() - } -} diff --git a/crates/cli/src/cmd/build.rs b/crates/cli/src/cmd/build.rs new file mode 100644 index 00000000..51e974ea --- /dev/null +++ b/crates/cli/src/cmd/build.rs @@ -0,0 +1,44 @@ +use miette::IntoDiagnostic; +use project::{config::Config, Project}; +use std::env; +use std::path::PathBuf; + +#[derive(clap::Args)] +/// Build an Aiken project at the given working directory. +pub struct Args { + /// Path to project + #[clap(short, long)] + directory: Option, +} + +pub fn exec(Args { directory }: Args) -> miette::Result<()> { + let project_path = if let Some(d) = directory { + d + } else { + env::current_dir().into_diagnostic()? + }; + + let config = Config::load(project_path.clone()).into_diagnostic()?; + + let mut project = Project::new(config, project_path); + + let build_result = project.build(); + + let warning_count = project.warnings.len(); + + for warning in project.warnings { + warning.report() + } + + if let Err(err) = build_result { + err.report(); + + miette::bail!( + "failed: {} error(s), {warning_count} warning(s)", + err.total(), + ); + }; + + println!("finished with {warning_count} warning(s)"); + return Ok(()); +} diff --git a/crates/cli/src/cmd/check.rs b/crates/cli/src/cmd/check.rs new file mode 100644 index 00000000..0b20e975 --- /dev/null +++ b/crates/cli/src/cmd/check.rs @@ -0,0 +1,46 @@ +use miette::IntoDiagnostic; +use project::{config::Config, Project}; +use std::env; +use std::path::PathBuf; + +// TODO: Refactor this to remove logic duplication with the 'build command' + +#[derive(clap::Args)] +/// Typecheck a project project +pub struct Args { + /// Path to project + #[clap(short, long)] + directory: Option, +} + +pub fn exec(Args { directory }: Args) -> miette::Result<()> { + let project_path = if let Some(d) = directory { + d + } else { + env::current_dir().into_diagnostic()? + }; + + let config = Config::load(project_path.clone()).into_diagnostic()?; + + let mut project = Project::new(config, project_path); + + let build_result = project.check(); + + let warning_count = project.warnings.len(); + + for warning in project.warnings { + warning.report() + } + + if let Err(err) = build_result { + err.report(); + + miette::bail!( + "failed: {} error(s), {warning_count} warning(s)", + err.total(), + ); + }; + + println!("finished with {warning_count} warning(s)"); + return Ok(()); +} diff --git a/crates/cli/src/cmd/mod.rs b/crates/cli/src/cmd/mod.rs new file mode 100644 index 00000000..f7fa2e10 --- /dev/null +++ b/crates/cli/src/cmd/mod.rs @@ -0,0 +1,5 @@ +pub mod build; +pub mod check; +pub mod new; +pub mod tx; +pub mod uplc; diff --git a/crates/cli/src/cmd/new.rs b/crates/cli/src/cmd/new.rs new file mode 100644 index 00000000..920aac1b --- /dev/null +++ b/crates/cli/src/cmd/new.rs @@ -0,0 +1,20 @@ +use miette::IntoDiagnostic; +use std::fs; +use std::path::PathBuf; + +#[derive(clap::Args)] +/// Create a new Aiken project +pub struct Args { + /// Project name + name: PathBuf, +} + +pub fn exec(Args { name }: Args) -> miette::Result<()> { + if !name.exists() { + fs::create_dir_all(name.join("lib")).into_diagnostic()?; + fs::create_dir_all(name.join("policies")).into_diagnostic()?; + fs::create_dir_all(name.join("scripts")).into_diagnostic()?; + } + + return Ok(()); +} diff --git a/crates/cli/src/cmd/tx/mod.rs b/crates/cli/src/cmd/tx/mod.rs new file mode 100644 index 00000000..e2d63a97 --- /dev/null +++ b/crates/cli/src/cmd/tx/mod.rs @@ -0,0 +1,15 @@ +pub mod simulate; + +use clap::Subcommand; + +/// Commands for working with transactions +#[derive(Subcommand)] +pub enum Cmd { + Simulate(simulate::Args), +} + +pub fn exec(cmd: Cmd) -> miette::Result<()> { + match cmd { + Cmd::Simulate(args) => simulate::exec(args), + } +} diff --git a/crates/cli/src/cmd/tx/simulate.rs b/crates/cli/src/cmd/tx/simulate.rs new file mode 100644 index 00000000..380a88e0 --- /dev/null +++ b/crates/cli/src/cmd/tx/simulate.rs @@ -0,0 +1,125 @@ +use miette::IntoDiagnostic; +use pallas_primitives::{ + babbage::{TransactionInput, TransactionOutput}, + Fragment, +}; +use pallas_traverse::{Era, MultiEraTx}; +use std::fs; +use std::path::PathBuf; +use uplc::{ + machine::cost_model::ExBudget, + tx::{ + self, + script_context::{ResolvedInput, SlotConfig}, + }, +}; + +#[derive(clap::Args)] +/// Simulate a transaction by evaluating it's script +pub struct Args { + /// A file containing cbor hex for a transaction + input: PathBuf, + + /// Toggle whether input is raw cbor or a hex string + #[clap(short, long)] + cbor: bool, + + /// A file containing cbor hex for the raw inputs + raw_inputs: PathBuf, + + /// A file containing cbor hex for the raw outputs + raw_outputs: PathBuf, + + /// Time between each slot + #[clap(short, long, default_value_t = 1000)] + slot_length: u32, + + /// Time of shelley hardfork + #[clap(long, default_value_t = 1596059091000)] + zero_time: u64, + + /// Slot number at the start of the shelley hardfork + #[clap(long, default_value_t = 4492800)] + zero_slot: u64, +} + +pub fn exec( + Args { + input, + cbor, + raw_inputs, + raw_outputs, + slot_length, + zero_time, + zero_slot, + }: Args, +) -> miette::Result<()> { + let (tx_bytes, inputs_bytes, outputs_bytes) = if cbor { + ( + fs::read(input).into_diagnostic()?, + fs::read(raw_inputs).into_diagnostic()?, + fs::read(raw_outputs).into_diagnostic()?, + ) + } else { + let cbor_hex = fs::read_to_string(input).into_diagnostic()?; + let inputs_hex = fs::read_to_string(raw_inputs).into_diagnostic()?; + let outputs_hex = fs::read_to_string(raw_outputs).into_diagnostic()?; + + ( + hex::decode(cbor_hex.trim()).into_diagnostic()?, + hex::decode(inputs_hex.trim()).into_diagnostic()?, + hex::decode(outputs_hex.trim()).into_diagnostic()?, + ) + }; + + let tx = MultiEraTx::decode(Era::Babbage, &tx_bytes) + .or_else(|_| MultiEraTx::decode(Era::Alonzo, &tx_bytes)) + .into_diagnostic()?; + + let inputs = Vec::::decode_fragment(&inputs_bytes).unwrap(); + let outputs = Vec::::decode_fragment(&outputs_bytes).unwrap(); + + let resolved_inputs: Vec = inputs + .iter() + .zip(outputs.iter()) + .map(|(input, output)| ResolvedInput { + input: input.clone(), + output: output.clone(), + }) + .collect(); + + println!("Simulating: {}", tx.hash()); + + if let Some(tx_babbage) = tx.as_babbage() { + let slot_config = SlotConfig { + zero_time, + zero_slot, + slot_length, + }; + + let result = + tx::eval_phase_two(tx_babbage, &resolved_inputs, None, None, &slot_config, true); + + match result { + Ok(redeemers) => { + println!("\nTotal Budget Used\n-----------------\n"); + + let total_budget_used = + redeemers + .iter() + .fold(ExBudget { mem: 0, cpu: 0 }, |accum, curr| ExBudget { + mem: accum.mem + curr.ex_units.mem as i64, + cpu: accum.cpu + curr.ex_units.steps as i64, + }); + + println!("mem: {}", total_budget_used.mem); + println!("cpu: {}", total_budget_used.cpu); + } + Err(err) => { + eprintln!("\nError\n-----\n\n{}\n", err); + } + } + } + + return Ok(()); +} diff --git a/crates/cli/src/cmd/uplc/eval.rs b/crates/cli/src/cmd/uplc/eval.rs new file mode 100644 index 00000000..d55cf2eb --- /dev/null +++ b/crates/cli/src/cmd/uplc/eval.rs @@ -0,0 +1,75 @@ +use miette::IntoDiagnostic; +use std::path::PathBuf; +use uplc::{ + ast::{FakeNamedDeBruijn, Name, NamedDeBruijn, Program, Term}, + machine::cost_model::ExBudget, + parser, +}; + +#[derive(clap::Args)] +/// Evaluate an Untyped Plutus Core program +pub struct Args { + script: PathBuf, + + #[clap(short, long)] + flat: bool, + + /// Arguments to pass to the uplc program + args: Vec, +} + +pub fn exec(Args { script, flat, args }: Args) -> miette::Result<()> { + let mut program = if flat { + let bytes = std::fs::read(&script).into_diagnostic()?; + + let prog = Program::::from_flat(&bytes).into_diagnostic()?; + + prog.into() + } else { + let code = std::fs::read_to_string(&script).into_diagnostic()?; + + let prog = parser::program(&code).into_diagnostic()?; + + Program::::try_from(prog).into_diagnostic()? + }; + + for arg in args { + let term: Term = parser::term(&arg) + .into_diagnostic()? + .try_into() + .into_diagnostic()?; + + program = program.apply_term(&term); + } + + let (term, cost, logs) = program.eval(); + + match term { + Ok(term) => { + let term: Term = term.try_into().into_diagnostic()?; + + println!("\nResult\n------\n\n{}\n", term.to_pretty()); + } + Err(err) => { + eprintln!("\nError\n-----\n\n{}\n", err); + } + } + + let budget = ExBudget::default(); + + println!( + "\nCosts\n-----\ncpu: {}\nmemory: {}", + budget.cpu - cost.cpu, + budget.mem - cost.mem + ); + println!( + "\nBudget\n------\ncpu: {}\nmemory: {}\n", + cost.cpu, cost.mem + ); + + if !logs.is_empty() { + println!("\nLogs\n----\n{}", logs.join("\n")) + } + + return Ok(()); +} diff --git a/crates/cli/src/cmd/uplc/flat.rs b/crates/cli/src/cmd/uplc/flat.rs new file mode 100644 index 00000000..2412994b --- /dev/null +++ b/crates/cli/src/cmd/uplc/flat.rs @@ -0,0 +1,83 @@ +use miette::IntoDiagnostic; +use std::{fmt::Write, fs, path::PathBuf}; +use uplc::{ + ast::{DeBruijn, Program}, + parser, +}; + +#[derive(clap::Args)] +/// Encode textual Untyped Plutus Core to flat bytes +pub struct Args { + /// Textual Untyped Plutus Core file + input: PathBuf, + + /// Output file name + #[clap(short, long)] + out: Option, + + /// Print output instead of saving to file + #[clap(short, long)] + print: bool, + + #[clap(short, long)] + cbor_hex: bool, +} + +pub fn exec( + Args { + input, + out, + print, + cbor_hex, + }: Args, +) -> miette::Result<()> { + let code = std::fs::read_to_string(&input).into_diagnostic()?; + + let program = parser::program(&code).into_diagnostic()?; + + let program = Program::::try_from(program).into_diagnostic()?; + + if !cbor_hex { + let bytes = program.to_flat().into_diagnostic()?; + + if print { + let mut output = String::new(); + + for (i, byte) in bytes.iter().enumerate() { + let _ = write!(output, "{:08b}", byte); + + if (i + 1) % 4 == 0 { + output.push('\n'); + } else { + output.push(' '); + } + } + + println!("{}", output); + } else { + let out_name = if let Some(out) = out { + out + } else { + format!("{}.flat", input.file_stem().unwrap().to_str().unwrap()) + }; + + fs::write(&out_name, &bytes).into_diagnostic()?; + } + } else { + let cbor = program.to_hex().into_diagnostic()?; + + if print { + println!("{}", &cbor); + } else { + let out_name = if let Some(out) = out { + out + } else { + format!("{}.cbor", input.file_stem().unwrap().to_str().unwrap()) + }; + + fs::write(&out_name, &cbor).into_diagnostic()?; + } + } + + return Ok(()); +} diff --git a/crates/cli/src/cmd/uplc/fmt.rs b/crates/cli/src/cmd/uplc/fmt.rs new file mode 100644 index 00000000..7391e49a --- /dev/null +++ b/crates/cli/src/cmd/uplc/fmt.rs @@ -0,0 +1,30 @@ +use miette::IntoDiagnostic; +use std::{fs, path::PathBuf}; +use uplc::parser; + +#[derive(clap::Args)] +/// Format an Untyped Plutus Core program +pub struct Args { + /// Textual Untyped Plutus Core file + input: PathBuf, + + /// Print output instead of saving to file + #[clap(short, long)] + print: bool, +} + +pub fn exec(Args { input, print }: Args) -> miette::Result<()> { + let code = std::fs::read_to_string(&input).into_diagnostic()?; + + let program = parser::program(&code).into_diagnostic()?; + + let pretty = program.to_pretty(); + + if print { + println!("{}", pretty); + } else { + fs::write(&input, pretty).into_diagnostic()?; + } + + return Ok(()); +} diff --git a/crates/cli/src/cmd/uplc/mod.rs b/crates/cli/src/cmd/uplc/mod.rs new file mode 100644 index 00000000..edb1c56b --- /dev/null +++ b/crates/cli/src/cmd/uplc/mod.rs @@ -0,0 +1,24 @@ +mod eval; +mod flat; +mod fmt; +mod unflat; + +use clap::Subcommand; + +/// Commands for working with untyped Plutus-core +#[derive(Subcommand)] +pub enum Cmd { + Fmt(fmt::Args), + Eval(eval::Args), + Flat(flat::Args), + Unflat(unflat::Args), +} + +pub fn exec(cmd: Cmd) -> miette::Result<()> { + match cmd { + Cmd::Fmt(args) => fmt::exec(args), + Cmd::Eval(args) => eval::exec(args), + Cmd::Flat(args) => flat::exec(args), + Cmd::Unflat(args) => unflat::exec(args), + } +} diff --git a/crates/cli/src/cmd/uplc/unflat.rs b/crates/cli/src/cmd/uplc/unflat.rs new file mode 100644 index 00000000..3f9abb6b --- /dev/null +++ b/crates/cli/src/cmd/uplc/unflat.rs @@ -0,0 +1,61 @@ +use miette::IntoDiagnostic; +use std::{fs, path::PathBuf}; +use uplc::ast::{DeBruijn, Name, Program}; + +#[derive(clap::Args)] +/// Decode flat bytes to textual Untyped Plutus Core +pub struct Args { + /// Flat encoded Untyped Plutus Core file + input: PathBuf, + + /// Output file name + #[clap(short, long)] + out: Option, + + /// Print output instead of saving to file + #[clap(short, long)] + print: bool, + + #[clap(short, long)] + cbor_hex: bool, +} + +pub fn exec( + Args { + input, + out, + print, + cbor_hex, + }: Args, +) -> miette::Result<()> { + let program = if cbor_hex { + let cbor = std::fs::read_to_string(&input).into_diagnostic()?; + + let mut cbor_buffer = Vec::new(); + let mut flat_buffer = Vec::new(); + + Program::::from_hex(cbor.trim(), &mut cbor_buffer, &mut flat_buffer) + .into_diagnostic()? + } else { + let bytes = std::fs::read(&input).into_diagnostic()?; + + Program::::from_flat(&bytes).into_diagnostic()? + }; + + let program: Program = program.try_into().into_diagnostic()?; + + let pretty = program.to_pretty(); + + if print { + println!("{}", pretty); + } else { + let out_name = if let Some(out) = out { + out + } else { + format!("{}.uplc", input.file_stem().unwrap().to_str().unwrap()) + }; + + fs::write(&out_name, pretty).into_diagnostic()?; + } + return Ok(()); +} diff --git a/crates/cli/src/lib.rs b/crates/cli/src/lib.rs deleted file mode 100644 index 362113c6..00000000 --- a/crates/cli/src/lib.rs +++ /dev/null @@ -1,2 +0,0 @@ -pub use aiken_lang; -pub use uplc; diff --git a/crates/cli/src/main.rs b/crates/cli/src/main.rs index dc28d59e..2e64ebf1 100644 --- a/crates/cli/src/main.rs +++ b/crates/cli/src/main.rs @@ -1,355 +1,37 @@ -use std::{env, fmt::Write as _, fs}; +mod cmd; -use miette::IntoDiagnostic; -use pallas_primitives::{ - babbage::{TransactionInput, TransactionOutput}, - Fragment, -}; -use pallas_traverse::{Era, MultiEraTx}; -use uplc::{ - ast::{DeBruijn, FakeNamedDeBruijn, Name, NamedDeBruijn, Program, Term}, - machine::cost_model::ExBudget, - parser, - tx::{ - self, - script_context::{ResolvedInput, SlotConfig}, - }, -}; +use clap::Parser; +use cmd::{build, check, new, tx, uplc}; -use project::{config::Config, Project}; +/// Aiken: a smart-contract language and toolchain for Cardano +#[derive(Parser)] +#[clap(version, about, long_about = None)] +#[clap(propagate_version = true)] +pub enum Cmd { + New(new::Args), + Build(build::Args), + Check(check::Args), -mod args; + #[clap(subcommand)] + Tx(tx::Cmd), -use args::{Args, TxCommand, UplcCommand}; + #[clap(subcommand)] + Uplc(uplc::Cmd), +} + +impl Default for Cmd { + fn default() -> Self { + Self::parse() + } +} fn main() -> miette::Result<()> { miette::set_panic_hook(); - - let args = Args::default(); - - match args { - Args::Build { directory } => { - let project_path = if let Some(d) = directory { - d - } else { - env::current_dir().into_diagnostic()? - }; - - let config = Config::load(project_path.clone()).into_diagnostic()?; - - let mut project = Project::new(config, project_path); - - let build_result = project.build(); - - let warning_count = project.warnings.len(); - - for warning in project.warnings { - warning.report() - } - - if let Err(err) = build_result { - err.report(); - - miette::bail!( - "failed: {} error(s), {warning_count} warning(s)", - err.total(), - ); - }; - - println!("finished with {warning_count} warning(s)") - } - - Args::Check { directory } => { - let project_path = if let Some(d) = directory { - d - } else { - env::current_dir().into_diagnostic()? - }; - - let config = Config::load(project_path.clone()).into_diagnostic()?; - - let mut project = Project::new(config, project_path); - - let build_result = project.check(); - - let warning_count = project.warnings.len(); - - for warning in project.warnings { - warning.report() - } - - if let Err(err) = build_result { - err.report(); - - miette::bail!( - "failed: {} error(s), {warning_count} warning(s)", - err.total(), - ); - }; - - println!("finished with {warning_count} warning(s)") - } - - Args::Dev => { - // launch a development server - // this should allow people to test - // their contracts over http - todo!() - } - - Args::New { name } => { - if !name.exists() { - fs::create_dir_all(name.join("lib")).into_diagnostic()?; - fs::create_dir_all(name.join("policies")).into_diagnostic()?; - fs::create_dir_all(name.join("scripts")).into_diagnostic()?; - } - } - - Args::Tx(tx_cmd) => match tx_cmd { - TxCommand::Simulate { - input, - cbor, - raw_inputs, - raw_outputs, - slot_length, - zero_time, - zero_slot, - } => { - let (tx_bytes, inputs_bytes, outputs_bytes) = if cbor { - ( - fs::read(input).into_diagnostic()?, - fs::read(raw_inputs).into_diagnostic()?, - fs::read(raw_outputs).into_diagnostic()?, - ) - } else { - let cbor_hex = fs::read_to_string(input).into_diagnostic()?; - let inputs_hex = fs::read_to_string(raw_inputs).into_diagnostic()?; - let outputs_hex = fs::read_to_string(raw_outputs).into_diagnostic()?; - - ( - hex::decode(cbor_hex.trim()).into_diagnostic()?, - hex::decode(inputs_hex.trim()).into_diagnostic()?, - hex::decode(outputs_hex.trim()).into_diagnostic()?, - ) - }; - - let tx = MultiEraTx::decode(Era::Babbage, &tx_bytes) - .or_else(|_| MultiEraTx::decode(Era::Alonzo, &tx_bytes)) - .into_diagnostic()?; - - let inputs = Vec::::decode_fragment(&inputs_bytes).unwrap(); - let outputs = Vec::::decode_fragment(&outputs_bytes).unwrap(); - - let resolved_inputs: Vec = inputs - .iter() - .zip(outputs.iter()) - .map(|(input, output)| ResolvedInput { - input: input.clone(), - output: output.clone(), - }) - .collect(); - - println!("Simulating: {}", tx.hash()); - - if let Some(tx_babbage) = tx.as_babbage() { - let slot_config = SlotConfig { - zero_time, - zero_slot, - slot_length, - }; - - let result = tx::eval_phase_two( - tx_babbage, - &resolved_inputs, - None, - None, - &slot_config, - true, - ); - - match result { - Ok(redeemers) => { - println!("\nTotal Budget Used\n-----------------\n"); - - let total_budget_used = redeemers.iter().fold( - ExBudget { mem: 0, cpu: 0 }, - |accum, curr| ExBudget { - mem: accum.mem + curr.ex_units.mem as i64, - cpu: accum.cpu + curr.ex_units.steps as i64, - }, - ); - - println!("mem: {}", total_budget_used.mem); - println!("cpu: {}", total_budget_used.cpu); - } - Err(err) => { - eprintln!("\nError\n-----\n\n{}\n", err); - } - } - } - } - }, - Args::Uplc(uplc_cmd) => match uplc_cmd { - UplcCommand::Flat { - input, - print, - out, - cbor_hex, - } => { - let code = std::fs::read_to_string(&input).into_diagnostic()?; - - let program = parser::program(&code).into_diagnostic()?; - - let program = Program::::try_from(program).into_diagnostic()?; - - if !cbor_hex { - let bytes = program.to_flat().into_diagnostic()?; - - if print { - let mut output = String::new(); - - for (i, byte) in bytes.iter().enumerate() { - let _ = write!(output, "{:08b}", byte); - - if (i + 1) % 4 == 0 { - output.push('\n'); - } else { - output.push(' '); - } - } - - println!("{}", output); - } else { - let out_name = if let Some(out) = out { - out - } else { - format!("{}.flat", input.file_stem().unwrap().to_str().unwrap()) - }; - - fs::write(&out_name, &bytes).into_diagnostic()?; - } - } else { - let cbor = program.to_hex().into_diagnostic()?; - - if print { - println!("{}", &cbor); - } else { - let out_name = if let Some(out) = out { - out - } else { - format!("{}.cbor", input.file_stem().unwrap().to_str().unwrap()) - }; - - fs::write(&out_name, &cbor).into_diagnostic()?; - } - } - } - - UplcCommand::Fmt { input, print } => { - let code = std::fs::read_to_string(&input).into_diagnostic()?; - - let program = parser::program(&code).into_diagnostic()?; - - let pretty = program.to_pretty(); - - if print { - println!("{}", pretty); - } else { - fs::write(&input, pretty).into_diagnostic()?; - } - } - UplcCommand::Unflat { - input, - print, - out, - cbor_hex, - } => { - let program = if cbor_hex { - let cbor = std::fs::read_to_string(&input).into_diagnostic()?; - - let mut cbor_buffer = Vec::new(); - let mut flat_buffer = Vec::new(); - - Program::::from_hex(cbor.trim(), &mut cbor_buffer, &mut flat_buffer) - .into_diagnostic()? - } else { - let bytes = std::fs::read(&input).into_diagnostic()?; - - Program::::from_flat(&bytes).into_diagnostic()? - }; - - let program: Program = program.try_into().into_diagnostic()?; - - let pretty = program.to_pretty(); - - if print { - println!("{}", pretty); - } else { - let out_name = if let Some(out) = out { - out - } else { - format!("{}.uplc", input.file_stem().unwrap().to_str().unwrap()) - }; - - fs::write(&out_name, pretty).into_diagnostic()?; - } - } - - UplcCommand::Eval { script, flat, args } => { - let mut program = if flat { - let bytes = std::fs::read(&script).into_diagnostic()?; - - let prog = Program::::from_flat(&bytes).into_diagnostic()?; - - prog.into() - } else { - let code = std::fs::read_to_string(&script).into_diagnostic()?; - - let prog = parser::program(&code).into_diagnostic()?; - - Program::::try_from(prog).into_diagnostic()? - }; - - for arg in args { - let term: Term = parser::term(&arg) - .into_diagnostic()? - .try_into() - .into_diagnostic()?; - - program = program.apply_term(&term); - } - - let (term, cost, logs) = program.eval(); - - match term { - Ok(term) => { - let term: Term = term.try_into().into_diagnostic()?; - - println!("\nResult\n------\n\n{}\n", term.to_pretty()); - } - Err(err) => { - eprintln!("\nError\n-----\n\n{}\n", err); - } - } - - let budget = ExBudget::default(); - - println!( - "\nCosts\n-----\ncpu: {}\nmemory: {}", - budget.cpu - cost.cpu, - budget.mem - cost.mem - ); - println!( - "\nBudget\n------\ncpu: {}\nmemory: {}\n", - cost.cpu, cost.mem - ); - - if !logs.is_empty() { - println!("\nLogs\n----\n{}", logs.join("\n")) - } - } - }, + match Cmd::default() { + Cmd::Build(args) => build::exec(args), + Cmd::Check(args) => check::exec(args), + Cmd::New(args) => new::exec(args), + Cmd::Tx(sub_cmd) => tx::exec(sub_cmd), + Cmd::Uplc(sub_cmd) => uplc::exec(sub_cmd), } - - Ok(()) } From 8d45b2a2f59e4c24bbddb13fa55efe10a5f3fe44 Mon Sep 17 00:00:00 2001 From: KtorZ Date: Fri, 28 Oct 2022 15:41:51 +0200 Subject: [PATCH 3/4] Enforce ordering of commands/sub-commands according to source By default, clap orders command alphabetically, which can be quite confusing when listing commands with `--help`: ``` SUBCOMMANDS: eval Evaluate an Untyped Plutus Core program flat Encode textual Untyped Plutus Core to flat bytes fmt Format an Untyped Plutus Core program help Print this message or the help of the given subcommand(s) unflat Decode flat bytes to textual Untyped Plutus Cor ``` It is possible to instrument clap to order commands in the same way they are declared in the source, giving us back the freedom to order and group them in a manner that makes sense, e.g.: ``` SUBCOMMANDS: fmt Format an Untyped Plutus Core program eval Evaluate an Untyped Plutus Core program flat Encode textual Untyped Plutus Core to flat bytes unflat Decode flat bytes to textual Untyped Plutus Cor help Print this message or the help of the given subcommand(s) ``` --- crates/cli/src/cmd/tx/mod.rs | 1 + crates/cli/src/cmd/uplc/mod.rs | 1 + crates/cli/src/main.rs | 3 ++- 3 files changed, 4 insertions(+), 1 deletion(-) diff --git a/crates/cli/src/cmd/tx/mod.rs b/crates/cli/src/cmd/tx/mod.rs index e2d63a97..ae03fece 100644 --- a/crates/cli/src/cmd/tx/mod.rs +++ b/crates/cli/src/cmd/tx/mod.rs @@ -4,6 +4,7 @@ use clap::Subcommand; /// Commands for working with transactions #[derive(Subcommand)] +#[clap(setting(clap::AppSettings::DeriveDisplayOrder))] pub enum Cmd { Simulate(simulate::Args), } diff --git a/crates/cli/src/cmd/uplc/mod.rs b/crates/cli/src/cmd/uplc/mod.rs index edb1c56b..3aaa8eb7 100644 --- a/crates/cli/src/cmd/uplc/mod.rs +++ b/crates/cli/src/cmd/uplc/mod.rs @@ -7,6 +7,7 @@ use clap::Subcommand; /// Commands for working with untyped Plutus-core #[derive(Subcommand)] +#[clap(setting(clap::AppSettings::DeriveDisplayOrder))] pub enum Cmd { Fmt(fmt::Args), Eval(eval::Args), diff --git a/crates/cli/src/main.rs b/crates/cli/src/main.rs index 2e64ebf1..1862a501 100644 --- a/crates/cli/src/main.rs +++ b/crates/cli/src/main.rs @@ -7,6 +7,7 @@ use cmd::{build, check, new, tx, uplc}; #[derive(Parser)] #[clap(version, about, long_about = None)] #[clap(propagate_version = true)] +#[clap(setting(clap::AppSettings::DeriveDisplayOrder))] pub enum Cmd { New(new::Args), Build(build::Args), @@ -28,9 +29,9 @@ impl Default for Cmd { fn main() -> miette::Result<()> { miette::set_panic_hook(); match Cmd::default() { + Cmd::New(args) => new::exec(args), Cmd::Build(args) => build::exec(args), Cmd::Check(args) => check::exec(args), - Cmd::New(args) => new::exec(args), Cmd::Tx(sub_cmd) => tx::exec(sub_cmd), Cmd::Uplc(sub_cmd) => uplc::exec(sub_cmd), } From 4316d5c382788a53a9932105362efab59f67dfc8 Mon Sep 17 00:00:00 2001 From: KtorZ Date: Fri, 28 Oct 2022 16:12:28 +0200 Subject: [PATCH 4/4] Factor out common project-logic between build and check. --- crates/cli/src/cmd/build.rs | 35 ++------------------------ crates/cli/src/cmd/check.rs | 37 ++-------------------------- crates/cli/src/cmd/new.rs | 2 +- crates/cli/src/cmd/tx/simulate.rs | 2 +- crates/cli/src/cmd/uplc/eval.rs | 2 +- crates/cli/src/cmd/uplc/flat.rs | 2 +- crates/cli/src/cmd/uplc/fmt.rs | 2 +- crates/cli/src/cmd/uplc/unflat.rs | 2 +- crates/cli/src/lib.rs | 41 +++++++++++++++++++++++++++++++ crates/cli/src/main.rs | 4 +-- 10 files changed, 52 insertions(+), 77 deletions(-) create mode 100644 crates/cli/src/lib.rs diff --git a/crates/cli/src/cmd/build.rs b/crates/cli/src/cmd/build.rs index 51e974ea..b16a0140 100644 --- a/crates/cli/src/cmd/build.rs +++ b/crates/cli/src/cmd/build.rs @@ -1,10 +1,7 @@ -use miette::IntoDiagnostic; -use project::{config::Config, Project}; -use std::env; use std::path::PathBuf; #[derive(clap::Args)] -/// Build an Aiken project at the given working directory. +/// Build an Aiken project pub struct Args { /// Path to project #[clap(short, long)] @@ -12,33 +9,5 @@ pub struct Args { } pub fn exec(Args { directory }: Args) -> miette::Result<()> { - let project_path = if let Some(d) = directory { - d - } else { - env::current_dir().into_diagnostic()? - }; - - let config = Config::load(project_path.clone()).into_diagnostic()?; - - let mut project = Project::new(config, project_path); - - let build_result = project.build(); - - let warning_count = project.warnings.len(); - - for warning in project.warnings { - warning.report() - } - - if let Err(err) = build_result { - err.report(); - - miette::bail!( - "failed: {} error(s), {warning_count} warning(s)", - err.total(), - ); - }; - - println!("finished with {warning_count} warning(s)"); - return Ok(()); + crate::with_project(directory, |p| p.build()) } diff --git a/crates/cli/src/cmd/check.rs b/crates/cli/src/cmd/check.rs index 0b20e975..9f99e617 100644 --- a/crates/cli/src/cmd/check.rs +++ b/crates/cli/src/cmd/check.rs @@ -1,12 +1,7 @@ -use miette::IntoDiagnostic; -use project::{config::Config, Project}; -use std::env; use std::path::PathBuf; -// TODO: Refactor this to remove logic duplication with the 'build command' - #[derive(clap::Args)] -/// Typecheck a project project +/// Type-check an Aiken project pub struct Args { /// Path to project #[clap(short, long)] @@ -14,33 +9,5 @@ pub struct Args { } pub fn exec(Args { directory }: Args) -> miette::Result<()> { - let project_path = if let Some(d) = directory { - d - } else { - env::current_dir().into_diagnostic()? - }; - - let config = Config::load(project_path.clone()).into_diagnostic()?; - - let mut project = Project::new(config, project_path); - - let build_result = project.check(); - - let warning_count = project.warnings.len(); - - for warning in project.warnings { - warning.report() - } - - if let Err(err) = build_result { - err.report(); - - miette::bail!( - "failed: {} error(s), {warning_count} warning(s)", - err.total(), - ); - }; - - println!("finished with {warning_count} warning(s)"); - return Ok(()); + crate::with_project(directory, |p| p.check()) } diff --git a/crates/cli/src/cmd/new.rs b/crates/cli/src/cmd/new.rs index 920aac1b..ab4ce26b 100644 --- a/crates/cli/src/cmd/new.rs +++ b/crates/cli/src/cmd/new.rs @@ -16,5 +16,5 @@ pub fn exec(Args { name }: Args) -> miette::Result<()> { fs::create_dir_all(name.join("scripts")).into_diagnostic()?; } - return Ok(()); + Ok(()) } diff --git a/crates/cli/src/cmd/tx/simulate.rs b/crates/cli/src/cmd/tx/simulate.rs index 380a88e0..8f7c4492 100644 --- a/crates/cli/src/cmd/tx/simulate.rs +++ b/crates/cli/src/cmd/tx/simulate.rs @@ -121,5 +121,5 @@ pub fn exec( } } - return Ok(()); + Ok(()) } diff --git a/crates/cli/src/cmd/uplc/eval.rs b/crates/cli/src/cmd/uplc/eval.rs index d55cf2eb..0f22cfee 100644 --- a/crates/cli/src/cmd/uplc/eval.rs +++ b/crates/cli/src/cmd/uplc/eval.rs @@ -71,5 +71,5 @@ pub fn exec(Args { script, flat, args }: Args) -> miette::Result<()> { println!("\nLogs\n----\n{}", logs.join("\n")) } - return Ok(()); + Ok(()) } diff --git a/crates/cli/src/cmd/uplc/flat.rs b/crates/cli/src/cmd/uplc/flat.rs index 2412994b..e7efe84c 100644 --- a/crates/cli/src/cmd/uplc/flat.rs +++ b/crates/cli/src/cmd/uplc/flat.rs @@ -79,5 +79,5 @@ pub fn exec( } } - return Ok(()); + Ok(()) } diff --git a/crates/cli/src/cmd/uplc/fmt.rs b/crates/cli/src/cmd/uplc/fmt.rs index 7391e49a..c5bf026a 100644 --- a/crates/cli/src/cmd/uplc/fmt.rs +++ b/crates/cli/src/cmd/uplc/fmt.rs @@ -26,5 +26,5 @@ pub fn exec(Args { input, print }: Args) -> miette::Result<()> { fs::write(&input, pretty).into_diagnostic()?; } - return Ok(()); + Ok(()) } diff --git a/crates/cli/src/cmd/uplc/unflat.rs b/crates/cli/src/cmd/uplc/unflat.rs index 3f9abb6b..b45c4721 100644 --- a/crates/cli/src/cmd/uplc/unflat.rs +++ b/crates/cli/src/cmd/uplc/unflat.rs @@ -57,5 +57,5 @@ pub fn exec( fs::write(&out_name, pretty).into_diagnostic()?; } - return Ok(()); + Ok(()) } diff --git a/crates/cli/src/lib.rs b/crates/cli/src/lib.rs new file mode 100644 index 00000000..84e28b39 --- /dev/null +++ b/crates/cli/src/lib.rs @@ -0,0 +1,41 @@ +pub mod cmd; + +use miette::IntoDiagnostic; +use project::{config::Config, Project}; +use std::env; +use std::path::PathBuf; + +pub fn with_project(directory: Option, mut action: A) -> miette::Result<()> +where + A: FnMut(&mut Project) -> Result<(), project::error::Error>, +{ + let project_path = if let Some(d) = directory { + d + } else { + env::current_dir().into_diagnostic()? + }; + + let config = Config::load(project_path.clone()).into_diagnostic()?; + + let mut project = Project::new(config, project_path); + + let build_result = action(&mut project); + + let warning_count = project.warnings.len(); + + for warning in project.warnings { + warning.report() + } + + if let Err(err) = build_result { + err.report(); + + miette::bail!( + "failed: {} error(s), {warning_count} warning(s)", + err.total(), + ); + }; + + println!("finished with {warning_count} warning(s)"); + Ok(()) +} diff --git a/crates/cli/src/main.rs b/crates/cli/src/main.rs index 1862a501..70d150e7 100644 --- a/crates/cli/src/main.rs +++ b/crates/cli/src/main.rs @@ -1,7 +1,5 @@ -mod cmd; - +use aiken::cmd::{build, check, new, tx, uplc}; use clap::Parser; -use cmd::{build, check, new, tx, uplc}; /// Aiken: a smart-contract language and toolchain for Cardano #[derive(Parser)]