From 9ab458dcc617f2f4b6e69168fa27894c697f6b54 Mon Sep 17 00:00:00 2001 From: rvcas Date: Thu, 16 Nov 2023 23:55:07 -0500 Subject: [PATCH 01/25] feat: delay typemismatch errors in the machine runtime to pass 2 of the conformance tests, we need to make sure that we aren't typechecking builtin arguments as arguments are applied. This switches push to by removing the call to check_type and then reworking all the associated unwrap methods on Value so that they return the same errors that were being returned before. --- crates/uplc/src/machine/runtime.rs | 286 ++++++++---------- crates/uplc/src/machine/value.rs | 165 +++++----- .../bad-syntax-1/bad-syntax-1.uplc.expected | 2 +- .../iteWrongCondTypePartiallyApplied.uplc | 7 + ...rongCondTypePartiallyApplied.uplc.expected | 4 + .../term/unlifting-unsat/unlifting-unsat.uplc | 2 + .../unlifting-unsat.uplc.expected | 1 + 7 files changed, 226 insertions(+), 241 deletions(-) create mode 100644 crates/uplc/test_data/conformance/evaluation/builtin/interleaving/iteWrongCondTypePartiallyApplied/iteWrongCondTypePartiallyApplied.uplc create mode 100644 crates/uplc/test_data/conformance/evaluation/builtin/interleaving/iteWrongCondTypePartiallyApplied/iteWrongCondTypePartiallyApplied.uplc.expected create mode 100644 crates/uplc/test_data/conformance/evaluation/term/unlifting-unsat/unlifting-unsat.uplc create mode 100644 crates/uplc/test_data/conformance/evaluation/term/unlifting-unsat/unlifting-unsat.uplc.expected diff --git a/crates/uplc/src/machine/runtime.rs b/crates/uplc/src/machine/runtime.rs index 6198b463..2f53c6cd 100644 --- a/crates/uplc/src/machine/runtime.rs +++ b/crates/uplc/src/machine/runtime.rs @@ -89,8 +89,6 @@ impl BuiltinRuntime { } pub fn push(&mut self, arg: Value) -> Result<(), Error> { - self.fun.check_type(&arg, &self.args)?; - self.args.push(arg); Ok(()) @@ -437,8 +435,8 @@ impl DefaultFunction { ) -> Result { match self { DefaultFunction::AddInteger => { - let arg1 = args[0].unwrap_integer(); - let arg2 = args[1].unwrap_integer(); + let arg1 = args[0].unwrap_integer()?; + let arg2 = args[1].unwrap_integer()?; let result = arg1 + arg2; @@ -447,8 +445,8 @@ impl DefaultFunction { Ok(value) } DefaultFunction::SubtractInteger => { - let arg1 = args[0].unwrap_integer(); - let arg2 = args[1].unwrap_integer(); + let arg1 = args[0].unwrap_integer()?; + let arg2 = args[1].unwrap_integer()?; let result = arg1 - arg2; @@ -457,8 +455,8 @@ impl DefaultFunction { Ok(value) } DefaultFunction::MultiplyInteger => { - let arg1 = args[0].unwrap_integer(); - let arg2 = args[1].unwrap_integer(); + let arg1 = args[0].unwrap_integer()?; + let arg2 = args[1].unwrap_integer()?; let result = arg1 * arg2; @@ -467,8 +465,8 @@ impl DefaultFunction { Ok(value) } DefaultFunction::DivideInteger => { - let arg1 = args[0].unwrap_integer(); - let arg2 = args[1].unwrap_integer(); + let arg1 = args[0].unwrap_integer()?; + let arg2 = args[1].unwrap_integer()?; if *arg2 != 0.into() { let (result, _) = arg1.div_mod_floor(arg2); @@ -481,8 +479,8 @@ impl DefaultFunction { } } DefaultFunction::QuotientInteger => { - let arg1 = args[0].unwrap_integer(); - let arg2 = args[1].unwrap_integer(); + let arg1 = args[0].unwrap_integer()?; + let arg2 = args[1].unwrap_integer()?; if *arg2 != 0.into() { let (result, _) = arg1.div_rem(arg2); @@ -495,8 +493,8 @@ impl DefaultFunction { } } DefaultFunction::RemainderInteger => { - let arg1 = args[0].unwrap_integer(); - let arg2 = args[1].unwrap_integer(); + let arg1 = args[0].unwrap_integer()?; + let arg2 = args[1].unwrap_integer()?; if *arg2 != 0.into() { let (_, result) = arg1.div_rem(arg2); @@ -509,8 +507,8 @@ impl DefaultFunction { } } DefaultFunction::ModInteger => { - let arg1 = args[0].unwrap_integer(); - let arg2 = args[1].unwrap_integer(); + let arg1 = args[0].unwrap_integer()?; + let arg2 = args[1].unwrap_integer()?; if *arg2 != 0.into() { let (_, result) = arg1.div_mod_floor(arg2); @@ -523,32 +521,32 @@ impl DefaultFunction { } } DefaultFunction::EqualsInteger => { - let arg1 = args[0].unwrap_integer(); - let arg2 = args[1].unwrap_integer(); + let arg1 = args[0].unwrap_integer()?; + let arg2 = args[1].unwrap_integer()?; let value = Value::bool(arg1 == arg2); Ok(value) } DefaultFunction::LessThanInteger => { - let arg1 = args[0].unwrap_integer(); - let arg2 = args[1].unwrap_integer(); + let arg1 = args[0].unwrap_integer()?; + let arg2 = args[1].unwrap_integer()?; let value = Value::bool(arg1 < arg2); Ok(value) } DefaultFunction::LessThanEqualsInteger => { - let arg1 = args[0].unwrap_integer(); - let arg2 = args[1].unwrap_integer(); + let arg1 = args[0].unwrap_integer()?; + let arg2 = args[1].unwrap_integer()?; let value = Value::bool(arg1 <= arg2); Ok(value) } DefaultFunction::AppendByteString => { - let arg1 = args[0].unwrap_byte_string(); - let arg2 = args[1].unwrap_byte_string(); + let arg1 = args[0].unwrap_byte_string()?; + let arg2 = args[1].unwrap_byte_string()?; let result = arg1.iter().copied().chain(arg2.iter().copied()).collect(); @@ -557,8 +555,8 @@ impl DefaultFunction { Ok(value) } DefaultFunction::ConsByteString => { - let arg1 = args[0].unwrap_integer(); - let arg2 = args[1].unwrap_byte_string(); + let arg1 = args[0].unwrap_integer()?; + let arg2 = args[1].unwrap_byte_string()?; let byte: u8 = match semantics { BuiltinSemantics::V1 => { @@ -584,9 +582,9 @@ impl DefaultFunction { Ok(value) } DefaultFunction::SliceByteString => { - let arg1 = args[0].unwrap_integer(); - let arg2 = args[1].unwrap_integer(); - let arg3 = args[2].unwrap_byte_string(); + let arg1 = args[0].unwrap_integer()?; + let arg2 = args[1].unwrap_integer()?; + let arg3 = args[2].unwrap_byte_string()?; let skip: usize = if arg1.lt(&0.into()) { 0 @@ -606,15 +604,15 @@ impl DefaultFunction { Ok(value) } DefaultFunction::LengthOfByteString => { - let arg1 = args[0].unwrap_byte_string(); + let arg1 = args[0].unwrap_byte_string()?; let value = Value::integer(arg1.len().into()); Ok(value) } DefaultFunction::IndexByteString => { - let arg1 = args[0].unwrap_byte_string(); - let arg2 = args[1].unwrap_integer(); + let arg1 = args[0].unwrap_byte_string()?; + let arg2 = args[1].unwrap_integer()?; let index: i128 = arg2.try_into().unwrap(); @@ -629,24 +627,24 @@ impl DefaultFunction { } } DefaultFunction::EqualsByteString => { - let arg1 = args[0].unwrap_byte_string(); - let arg2 = args[1].unwrap_byte_string(); + let arg1 = args[0].unwrap_byte_string()?; + let arg2 = args[1].unwrap_byte_string()?; let value = Value::bool(arg1 == arg2); Ok(value) } DefaultFunction::LessThanByteString => { - let arg1 = args[0].unwrap_byte_string(); - let arg2 = args[1].unwrap_byte_string(); + let arg1 = args[0].unwrap_byte_string()?; + let arg2 = args[1].unwrap_byte_string()?; let value = Value::bool(arg1 < arg2); Ok(value) } DefaultFunction::LessThanEqualsByteString => { - let arg1 = args[0].unwrap_byte_string(); - let arg2 = args[1].unwrap_byte_string(); + let arg1 = args[0].unwrap_byte_string()?; + let arg2 = args[1].unwrap_byte_string()?; let value = Value::bool(arg1 <= arg2); @@ -655,7 +653,7 @@ impl DefaultFunction { DefaultFunction::Sha2_256 => { use cryptoxide::{digest::Digest, sha2::Sha256}; - let arg1 = args[0].unwrap_byte_string(); + let arg1 = args[0].unwrap_byte_string()?; let mut hasher = Sha256::new(); @@ -672,7 +670,7 @@ impl DefaultFunction { DefaultFunction::Sha3_256 => { use cryptoxide::{digest::Digest, sha3::Sha3_256}; - let arg1 = args[0].unwrap_byte_string(); + let arg1 = args[0].unwrap_byte_string()?; let mut hasher = Sha3_256::new(); @@ -690,7 +688,7 @@ impl DefaultFunction { DefaultFunction::Blake2b_224 => { use cryptoxide::{blake2b::Blake2b, digest::Digest}; - let arg1 = args[0].unwrap_byte_string(); + let arg1 = args[0].unwrap_byte_string()?; let mut digest = [0u8; 28]; let mut context = Blake2b::new(28); @@ -705,7 +703,7 @@ impl DefaultFunction { DefaultFunction::Blake2b_256 => { use cryptoxide::{blake2b::Blake2b, digest::Digest}; - let arg1 = args[0].unwrap_byte_string(); + let arg1 = args[0].unwrap_byte_string()?; let mut digest = [0u8; 32]; let mut context = Blake2b::new(32); @@ -720,7 +718,7 @@ impl DefaultFunction { DefaultFunction::Keccak_256 => { use cryptoxide::{digest::Digest, sha3::Keccak256}; - let arg1 = args[0].unwrap_byte_string(); + let arg1 = args[0].unwrap_byte_string()?; let mut hasher = Keccak256::new(); @@ -737,9 +735,9 @@ impl DefaultFunction { DefaultFunction::VerifyEd25519Signature => { use cryptoxide::ed25519; - let public_key = args[0].unwrap_byte_string(); - let message = args[1].unwrap_byte_string(); - let signature = args[2].unwrap_byte_string(); + let public_key = args[0].unwrap_byte_string()?; + let message = args[1].unwrap_byte_string()?; + let signature = args[2].unwrap_byte_string()?; let public_key: [u8; 32] = public_key .clone() @@ -758,37 +756,37 @@ impl DefaultFunction { Ok(value) } DefaultFunction::VerifyEcdsaSecp256k1Signature => { - let public_key = args[0].unwrap_byte_string(); - let message = args[1].unwrap_byte_string(); - let signature = args[2].unwrap_byte_string(); + let public_key = args[0].unwrap_byte_string()?; + let message = args[1].unwrap_byte_string()?; + let signature = args[2].unwrap_byte_string()?; verify_ecdsa(public_key, message, signature) } DefaultFunction::VerifySchnorrSecp256k1Signature => { - let public_key = args[0].unwrap_byte_string(); - let message = args[1].unwrap_byte_string(); - let signature = args[2].unwrap_byte_string(); + let public_key = args[0].unwrap_byte_string()?; + let message = args[1].unwrap_byte_string()?; + let signature = args[2].unwrap_byte_string()?; verify_schnorr(public_key, message, signature) } DefaultFunction::AppendString => { - let arg1 = args[0].unwrap_string(); - let arg2 = args[1].unwrap_string(); + let arg1 = args[0].unwrap_string()?; + let arg2 = args[1].unwrap_string()?; let value = Value::string(format!("{arg1}{arg2}")); Ok(value) } DefaultFunction::EqualsString => { - let arg1 = args[0].unwrap_string(); - let arg2 = args[1].unwrap_string(); + let arg1 = args[0].unwrap_string()?; + let arg2 = args[1].unwrap_string()?; let value = Value::bool(arg1 == arg2); Ok(value) } DefaultFunction::EncodeUtf8 => { - let arg1 = args[0].unwrap_string(); + let arg1 = args[0].unwrap_string()?; let bytes = arg1.as_bytes().to_vec(); @@ -797,7 +795,7 @@ impl DefaultFunction { Ok(value) } DefaultFunction::DecodeUtf8 => { - let arg1 = args[0].unwrap_byte_string(); + let arg1 = args[0].unwrap_byte_string()?; let string = String::from_utf8(arg1.clone())?; @@ -806,7 +804,7 @@ impl DefaultFunction { Ok(value) } DefaultFunction::IfThenElse => { - let condition = args[0].unwrap_bool(); + let condition = args[0].unwrap_bool()?; if *condition { Ok(args[1].clone()) @@ -815,35 +813,33 @@ impl DefaultFunction { } } DefaultFunction::ChooseUnit => { - // We don't need to check it here because this is already done in - // expect_type - // let Value::Con(Constant::Unit) = args[0] else {unreachable!()}; + args[0].unwrap_unit()?; Ok(args[1].clone()) } DefaultFunction::Trace => { - let arg1 = args[0].unwrap_string(); + let arg1 = args[0].unwrap_string()?; logs.push(arg1.clone()); Ok(args[1].clone()) } DefaultFunction::FstPair => { - let (_, _, first, _) = args[0].unwrap_pair(); + let (_, _, first, _) = args[0].unwrap_pair()?; let value = Value::Con(first.clone()); Ok(value) } DefaultFunction::SndPair => { - let (_, _, _, second) = args[0].unwrap_pair(); + let (_, _, _, second) = args[0].unwrap_pair()?; let value = Value::Con(second.clone()); Ok(value) } DefaultFunction::ChooseList => { - let (_, list) = args[0].unwrap_list(); + let (_, list) = args[0].unwrap_list()?; if list.is_empty() { Ok(args[1].clone()) @@ -852,8 +848,12 @@ impl DefaultFunction { } } DefaultFunction::MkCons => { - let item = args[0].unwrap_constant(); - let (r#type, list) = args[1].unwrap_list(); + let item = args[0].unwrap_constant()?; + let (r#type, list) = args[1].unwrap_list()?; + + if *r#type != Type::from(item) { + return Err(Error::TypeMismatch(Type::from(item), r#type.clone())); + } let mut ret = vec![item.clone()]; @@ -864,15 +864,10 @@ impl DefaultFunction { Ok(value) } DefaultFunction::HeadList => { - let c @ Value::Con(inner) = &args[0] else { - unreachable!() - }; - let Constant::ProtoList(_, list) = inner.as_ref() else { - unreachable!() - }; + let (_, list) = args[0].unwrap_list()?; if list.is_empty() { - Err(Error::EmptyList(c.clone())) + Err(Error::EmptyList(args[0].clone())) } else { let value = Value::Con(list[0].clone().into()); @@ -880,15 +875,10 @@ impl DefaultFunction { } } DefaultFunction::TailList => { - let c @ Value::Con(inner) = &args[0] else { - unreachable!() - }; - let Constant::ProtoList(r#type, list) = inner.as_ref() else { - unreachable!() - }; + let (r#type, list) = args[0].unwrap_list()?; if list.is_empty() { - Err(Error::EmptyList(c.clone())) + Err(Error::EmptyList(args[0].clone())) } else { let value = Value::list(r#type.clone(), list[1..].to_vec()); @@ -896,14 +886,14 @@ impl DefaultFunction { } } DefaultFunction::NullList => { - let (_, list) = args[0].unwrap_list(); + let (_, list) = args[0].unwrap_list()?; let value = Value::bool(list.is_empty()); Ok(value) } DefaultFunction::ChooseData => { - let con = args[0].unwrap_constant(); + let con = args[0].unwrap_constant()?; match con { Constant::Data(PlutusData::Constr(_)) => Ok(args[1].clone()), @@ -915,8 +905,8 @@ impl DefaultFunction { } } DefaultFunction::ConstrData => { - let i = args[0].unwrap_integer(); - let l = args[1].unwrap_data_list(); + let i = args[0].unwrap_integer()?; + let l = args[1].unwrap_data_list()?; let data_list: Vec = l .iter() @@ -939,7 +929,7 @@ impl DefaultFunction { Ok(value) } DefaultFunction::MapData => { - let (_, list) = args[0].unwrap_list(); + let (_, list) = args[0].unwrap_list()?; let mut map = Vec::new(); @@ -961,7 +951,7 @@ impl DefaultFunction { Ok(value) } DefaultFunction::ListData => { - let (_, list) = args[0].unwrap_list(); + let (_, list) = args[0].unwrap_list()?; let data_list: Vec = list .iter() @@ -976,14 +966,14 @@ impl DefaultFunction { Ok(value) } DefaultFunction::IData => { - let i = args[0].unwrap_integer(); + let i = args[0].unwrap_integer()?; let value = Value::data(PlutusData::BigInt(to_pallas_bigint(i))); Ok(value) } DefaultFunction::BData => { - let b = args[0].unwrap_byte_string(); + let b = args[0].unwrap_byte_string()?; let value = Value::data(PlutusData::BoundedBytes(b.clone().try_into().unwrap())); @@ -1022,10 +1012,7 @@ impl DefaultFunction { Ok(value) } - v => Err(Error::DeserialisationError( - "UnConstrData".to_string(), - v.clone(), - )), + v => Err(Error::NotAConstant(v.clone())), }, DefaultFunction::UnMapData => match &args[0] { v @ Value::Con(inner) => { @@ -1055,10 +1042,7 @@ impl DefaultFunction { Ok(value) } - v => Err(Error::DeserialisationError( - "UnMapData".to_string(), - v.clone(), - )), + v => Err(Error::NotAConstant(v.clone())), }, DefaultFunction::UnListData => match &args[0] { v @ Value::Con(inner) => { @@ -1079,10 +1063,7 @@ impl DefaultFunction { Ok(value) } - v => Err(Error::DeserialisationError( - "UnListData".to_string(), - v.clone(), - )), + v => Err(Error::NotAConstant(v.clone())), }, DefaultFunction::UnIData => match &args[0] { v @ Value::Con(inner) => { @@ -1097,10 +1078,7 @@ impl DefaultFunction { Ok(value) } - v => Err(Error::DeserialisationError( - "UnIData".to_string(), - v.clone(), - )), + v => Err(Error::NotAConstant(v.clone())), }, DefaultFunction::UnBData => match &args[0] { v @ Value::Con(inner) => { @@ -1115,34 +1093,18 @@ impl DefaultFunction { Ok(value) } - v => Err(Error::DeserialisationError( - "UnBData".to_string(), - v.clone(), - )), + v => Err(Error::NotAConstant(v.clone())), }, DefaultFunction::EqualsData => { - let (Value::Con(inner1), Value::Con(inner2)) = (&args[0], &args[1]) else { - unreachable!() - }; - - let Constant::Data(d1) = inner1.as_ref() else { - unreachable!() - }; - let Constant::Data(d2) = inner2.as_ref() else { - unreachable!() - }; + let d1 = args[0].unwrap_data()?; + let d2 = args[1].unwrap_data()?; let value = Value::bool(d1.eq(d2)); Ok(value) } DefaultFunction::SerialiseData => { - let Value::Con(inner) = &args[0] else { - unreachable!() - }; - let Constant::Data(d) = inner.as_ref() else { - unreachable!() - }; + let d = args[0].unwrap_data()?; let serialized_data = plutus_data_to_bytes(d).unwrap(); @@ -1151,16 +1113,8 @@ impl DefaultFunction { Ok(value) } DefaultFunction::MkPairData => { - let (Value::Con(inner1), Value::Con(inner2)) = (&args[0], &args[1]) else { - unreachable!() - }; - - let Constant::Data(d1) = inner1.as_ref() else { - unreachable!() - }; - let Constant::Data(d2) = inner2.as_ref() else { - unreachable!() - }; + let d1 = args[0].unwrap_data()?; + let d2 = args[1].unwrap_data()?; let constant = Constant::ProtoPair( Type::Data, @@ -1174,11 +1128,15 @@ impl DefaultFunction { Ok(value) } DefaultFunction::MkNilData => { + args[0].unwrap_unit()?; + let value = Value::list(Type::Data, vec![]); Ok(value) } DefaultFunction::MkNilPairData => { + args[0].unwrap_unit()?; + let constant = Constant::ProtoList( Type::Pair(Rc::new(Type::Data), Rc::new(Type::Data)), vec![], @@ -1190,8 +1148,8 @@ impl DefaultFunction { } DefaultFunction::Bls12_381_G1_Add => { - let arg1 = args[0].unwrap_bls12_381_g1_element(); - let arg2 = args[1].unwrap_bls12_381_g1_element(); + let arg1 = args[0].unwrap_bls12_381_g1_element()?; + let arg2 = args[1].unwrap_bls12_381_g1_element()?; let mut out = blst::blst_p1::default(); @@ -1208,7 +1166,7 @@ impl DefaultFunction { Ok(Value::Con(constant.into())) } DefaultFunction::Bls12_381_G1_Neg => { - let arg1 = args[0].unwrap_bls12_381_g1_element(); + let arg1 = args[0].unwrap_bls12_381_g1_element()?; let mut out = *arg1; @@ -1225,8 +1183,8 @@ impl DefaultFunction { Ok(Value::Con(constant.into())) } DefaultFunction::Bls12_381_G1_ScalarMul => { - let arg1 = args[0].unwrap_integer(); - let arg2 = args[1].unwrap_bls12_381_g1_element(); + let arg1 = args[0].unwrap_integer()?; + let arg2 = args[1].unwrap_bls12_381_g1_element()?; let size_scalar = size_of::(); @@ -1266,8 +1224,8 @@ impl DefaultFunction { Ok(Value::Con(constant.into())) } DefaultFunction::Bls12_381_G1_Equal => { - let arg1 = args[0].unwrap_bls12_381_g1_element(); - let arg2 = args[1].unwrap_bls12_381_g1_element(); + let arg1 = args[0].unwrap_bls12_381_g1_element()?; + let arg2 = args[1].unwrap_bls12_381_g1_element()?; let is_equal = unsafe { blst::blst_p1_is_equal(arg1, arg2) }; @@ -1276,7 +1234,7 @@ impl DefaultFunction { Ok(Value::Con(constant.into())) } DefaultFunction::Bls12_381_G1_Compress => { - let arg1 = args[0].unwrap_bls12_381_g1_element(); + let arg1 = args[0].unwrap_bls12_381_g1_element()?; let out = arg1.compress(); @@ -1285,7 +1243,7 @@ impl DefaultFunction { Ok(Value::Con(constant.into())) } DefaultFunction::Bls12_381_G1_Uncompress => { - let arg1 = args[0].unwrap_byte_string(); + let arg1 = args[0].unwrap_byte_string()?; let out = blst::blst_p1::uncompress(arg1)?; @@ -1294,8 +1252,8 @@ impl DefaultFunction { Ok(Value::Con(constant.into())) } DefaultFunction::Bls12_381_G1_HashToGroup => { - let arg1 = args[0].unwrap_byte_string(); - let arg2 = args[1].unwrap_byte_string(); + let arg1 = args[0].unwrap_byte_string()?; + let arg2 = args[1].unwrap_byte_string()?; if arg2.len() > 255 { return Err(Error::HashToCurveDstTooBig); @@ -1321,8 +1279,8 @@ impl DefaultFunction { Ok(Value::Con(constant.into())) } DefaultFunction::Bls12_381_G2_Add => { - let arg1 = args[0].unwrap_bls12_381_g2_element(); - let arg2 = args[1].unwrap_bls12_381_g2_element(); + let arg1 = args[0].unwrap_bls12_381_g2_element()?; + let arg2 = args[1].unwrap_bls12_381_g2_element()?; let mut out = blst::blst_p2::default(); @@ -1339,7 +1297,7 @@ impl DefaultFunction { Ok(Value::Con(constant.into())) } DefaultFunction::Bls12_381_G2_Neg => { - let arg1 = args[0].unwrap_bls12_381_g2_element(); + let arg1 = args[0].unwrap_bls12_381_g2_element()?; let mut out = *arg1; @@ -1356,8 +1314,8 @@ impl DefaultFunction { Ok(Value::Con(constant.into())) } DefaultFunction::Bls12_381_G2_ScalarMul => { - let arg1 = args[0].unwrap_integer(); - let arg2 = args[1].unwrap_bls12_381_g2_element(); + let arg1 = args[0].unwrap_integer()?; + let arg2 = args[1].unwrap_bls12_381_g2_element()?; let size_scalar = size_of::(); @@ -1397,8 +1355,8 @@ impl DefaultFunction { Ok(Value::Con(constant.into())) } DefaultFunction::Bls12_381_G2_Equal => { - let arg1 = args[0].unwrap_bls12_381_g2_element(); - let arg2 = args[1].unwrap_bls12_381_g2_element(); + let arg1 = args[0].unwrap_bls12_381_g2_element()?; + let arg2 = args[1].unwrap_bls12_381_g2_element()?; let is_equal = unsafe { blst::blst_p2_is_equal(arg1, arg2) }; @@ -1407,7 +1365,7 @@ impl DefaultFunction { Ok(Value::Con(constant.into())) } DefaultFunction::Bls12_381_G2_Compress => { - let arg1 = args[0].unwrap_bls12_381_g2_element(); + let arg1 = args[0].unwrap_bls12_381_g2_element()?; let out = arg1.compress(); @@ -1416,7 +1374,7 @@ impl DefaultFunction { Ok(Value::Con(constant.into())) } DefaultFunction::Bls12_381_G2_Uncompress => { - let arg1 = args[0].unwrap_byte_string(); + let arg1 = args[0].unwrap_byte_string()?; let out = blst::blst_p2::uncompress(arg1)?; @@ -1425,8 +1383,8 @@ impl DefaultFunction { Ok(Value::Con(constant.into())) } DefaultFunction::Bls12_381_G2_HashToGroup => { - let arg1 = args[0].unwrap_byte_string(); - let arg2 = args[1].unwrap_byte_string(); + let arg1 = args[0].unwrap_byte_string()?; + let arg2 = args[1].unwrap_byte_string()?; if arg2.len() > 255 { return Err(Error::HashToCurveDstTooBig); @@ -1452,8 +1410,8 @@ impl DefaultFunction { Ok(Value::Con(constant.into())) } DefaultFunction::Bls12_381_MillerLoop => { - let arg1 = args[0].unwrap_bls12_381_g1_element(); - let arg2 = args[1].unwrap_bls12_381_g2_element(); + let arg1 = args[0].unwrap_bls12_381_g1_element()?; + let arg2 = args[1].unwrap_bls12_381_g2_element()?; let mut out = blst::blst_fp12::default(); @@ -1472,8 +1430,8 @@ impl DefaultFunction { Ok(Value::Con(constant.into())) } DefaultFunction::Bls12_381_MulMlResult => { - let arg1 = args[0].unwrap_bls12_381_ml_result(); - let arg2 = args[1].unwrap_bls12_381_ml_result(); + let arg1 = args[0].unwrap_bls12_381_ml_result()?; + let arg2 = args[1].unwrap_bls12_381_ml_result()?; let mut out = blst::blst_fp12::default(); @@ -1486,8 +1444,8 @@ impl DefaultFunction { Ok(Value::Con(constant.into())) } DefaultFunction::Bls12_381_FinalVerify => { - let arg1 = args[0].unwrap_bls12_381_ml_result(); - let arg2 = args[1].unwrap_bls12_381_ml_result(); + let arg1 = args[0].unwrap_bls12_381_ml_result()?; + let arg2 = args[1].unwrap_bls12_381_ml_result()?; let verified = unsafe { blst::blst_fp12_finalverify(arg1, arg2) }; diff --git a/crates/uplc/src/machine/value.rs b/crates/uplc/src/machine/value.rs index f322b0ba..6e54f6fe 100644 --- a/crates/uplc/src/machine/value.rs +++ b/crates/uplc/src/machine/value.rs @@ -69,125 +69,138 @@ impl Value { Value::Con(constant.into()) } - pub(super) fn unwrap_integer(&self) -> &BigInt { - let Value::Con(inner) = self else { - unreachable!() - }; - let Constant::Integer(integer) = inner.as_ref() else { - unreachable!() + pub(super) fn unwrap_integer(&self) -> Result<&BigInt, Error> { + let inner = self.unwrap_constant()?; + + let Constant::Integer(integer) = inner else { + return Err(Error::TypeMismatch(Type::Integer, inner.into())); }; - integer + Ok(integer) } - pub(super) fn unwrap_byte_string(&self) -> &Vec { - let Value::Con(inner) = self else { - unreachable!() - }; - let Constant::ByteString(byte_string) = inner.as_ref() else { - unreachable!() + pub(super) fn unwrap_byte_string(&self) -> Result<&Vec, Error> { + let inner = self.unwrap_constant()?; + + let Constant::ByteString(byte_string) = inner else { + return Err(Error::TypeMismatch(Type::ByteString, inner.into())); }; - byte_string + Ok(byte_string) } - pub(super) fn unwrap_string(&self) -> &String { - let Value::Con(inner) = self else { - unreachable!() - }; - let Constant::String(string) = inner.as_ref() else { - unreachable!() + pub(super) fn unwrap_string(&self) -> Result<&String, Error> { + let inner = self.unwrap_constant()?; + + let Constant::String(string) = inner else { + return Err(Error::TypeMismatch(Type::String, inner.into())); }; - string + Ok(string) } - pub(super) fn unwrap_bool(&self) -> &bool { - let Value::Con(inner) = self else { - unreachable!() - }; - let Constant::Bool(condition) = inner.as_ref() else { - unreachable!() + pub(super) fn unwrap_bool(&self) -> Result<&bool, Error> { + let inner = self.unwrap_constant()?; + + let Constant::Bool(condition) = inner else { + return Err(Error::TypeMismatch(Type::Bool, inner.into())); }; - condition + Ok(condition) } - pub(super) fn unwrap_pair(&self) -> (&Type, &Type, &Rc, &Rc) { - let Value::Con(inner) = self else { - unreachable!() - }; - let Constant::ProtoPair(t1, t2, first, second) = inner.as_ref() else { - unreachable!() + #[allow(clippy::type_complexity)] + pub(super) fn unwrap_pair( + &self, + ) -> Result<(&Type, &Type, &Rc, &Rc), Error> { + let inner = self.unwrap_constant()?; + + let Constant::ProtoPair(t1, t2, first, second) = inner else { + return Err(Error::PairTypeMismatch(inner.into())); }; - (t1, t2, first, second) + Ok((t1, t2, first, second)) } - pub(super) fn unwrap_list(&self) -> (&Type, &Vec) { - let Value::Con(inner) = self else { - unreachable!() - }; - let Constant::ProtoList(t, list) = inner.as_ref() else { - unreachable!() + pub(super) fn unwrap_list(&self) -> Result<(&Type, &Vec), Error> { + let inner = self.unwrap_constant()?; + + let Constant::ProtoList(t, list) = inner else { + return Err(Error::ListTypeMismatch(inner.into())); }; - (t, list) + Ok((t, list)) } - pub(super) fn unwrap_constant(&self) -> &Constant { + pub(super) fn unwrap_data(&self) -> Result<&PlutusData, Error> { + let inner = self.unwrap_constant()?; + + let Constant::Data(data) = inner else { + return Err(Error::TypeMismatch(Type::Data, inner.into())); + }; + + Ok(data) + } + + pub(super) fn unwrap_unit(&self) -> Result<(), Error> { + let inner = self.unwrap_constant()?; + + let Constant::Unit = inner else { + return Err(Error::TypeMismatch(Type::Unit, inner.into())); + }; + + Ok(()) + } + + pub(super) fn unwrap_constant(&self) -> Result<&Constant, Error> { let Value::Con(item) = self else { - unreachable!() + return Err(Error::NotAConstant(self.clone())); }; - item.as_ref() + Ok(item.as_ref()) } - pub(super) fn unwrap_data_list(&self) -> &Vec { - let Value::Con(inner) = self else { - unreachable!() - }; - let Constant::ProtoList(Type::Data, list) = inner.as_ref() else { - unreachable!() + pub(super) fn unwrap_data_list(&self) -> Result<&Vec, Error> { + let inner = self.unwrap_constant()?; + + let Constant::ProtoList(Type::Data, list) = inner else { + return Err(Error::TypeMismatch( + Type::List(Type::Data.into()), + inner.into(), + )); }; - list + Ok(list) } - pub(super) fn unwrap_bls12_381_g1_element(&self) -> &blst::blst_p1 { - let Value::Con(inner) = self else { - unreachable!() + pub(super) fn unwrap_bls12_381_g1_element(&self) -> Result<&blst::blst_p1, Error> { + let inner = self.unwrap_constant()?; + + let Constant::Bls12_381G1Element(element) = inner else { + return Err(Error::TypeMismatch(Type::Bls12_381G1Element, inner.into())); }; - let Constant::Bls12_381G1Element(element) = inner.as_ref() else { - unreachable!() - }; - - element + Ok(element) } - pub(super) fn unwrap_bls12_381_g2_element(&self) -> &blst::blst_p2 { - let Value::Con(inner) = self else { - unreachable!() + pub(super) fn unwrap_bls12_381_g2_element(&self) -> Result<&blst::blst_p2, Error> { + let inner = self.unwrap_constant()?; + + let Constant::Bls12_381G2Element(element) = inner else { + return Err(Error::TypeMismatch(Type::Bls12_381G2Element, inner.into())); }; - let Constant::Bls12_381G2Element(element) = inner.as_ref() else { - unreachable!() - }; - - element + Ok(element) } - pub(super) fn unwrap_bls12_381_ml_result(&self) -> &blst::blst_fp12 { - let Value::Con(inner) = self else { - unreachable!() + pub(super) fn unwrap_bls12_381_ml_result(&self) -> Result<&blst::blst_fp12, Error> { + let inner = self.unwrap_constant()?; + + let Constant::Bls12_381MlResult(element) = inner else { + return Err(Error::TypeMismatch(Type::Bls12_381MlResult, inner.into())); }; - let Constant::Bls12_381MlResult(element) = inner.as_ref() else { - unreachable!() - }; - - element + Ok(element) } pub fn is_integer(&self) -> bool { diff --git a/crates/uplc/test_data/conformance/evaluation/builtin/constant/bls12-381/G1/bad-syntax-1/bad-syntax-1.uplc.expected b/crates/uplc/test_data/conformance/evaluation/builtin/constant/bls12-381/G1/bad-syntax-1/bad-syntax-1.uplc.expected index 13b380d3..fd569489 100644 --- a/crates/uplc/test_data/conformance/evaluation/builtin/constant/bls12-381/G1/bad-syntax-1/bad-syntax-1.uplc.expected +++ b/crates/uplc/test_data/conformance/evaluation/builtin/constant/bls12-381/G1/bad-syntax-1/bad-syntax-1.uplc.expected @@ -1 +1 @@ -parse error +parse error \ No newline at end of file diff --git a/crates/uplc/test_data/conformance/evaluation/builtin/interleaving/iteWrongCondTypePartiallyApplied/iteWrongCondTypePartiallyApplied.uplc b/crates/uplc/test_data/conformance/evaluation/builtin/interleaving/iteWrongCondTypePartiallyApplied/iteWrongCondTypePartiallyApplied.uplc new file mode 100644 index 00000000..b356a921 --- /dev/null +++ b/crates/uplc/test_data/conformance/evaluation/builtin/interleaving/iteWrongCondTypePartiallyApplied/iteWrongCondTypePartiallyApplied.uplc @@ -0,0 +1,7 @@ +(program + 1.0.0 + [ + [ (force (builtin ifThenElse)) (con string "11 <= 22") ] + (con string "\172(11 <= 22)") + ] +) diff --git a/crates/uplc/test_data/conformance/evaluation/builtin/interleaving/iteWrongCondTypePartiallyApplied/iteWrongCondTypePartiallyApplied.uplc.expected b/crates/uplc/test_data/conformance/evaluation/builtin/interleaving/iteWrongCondTypePartiallyApplied/iteWrongCondTypePartiallyApplied.uplc.expected new file mode 100644 index 00000000..aead86ec --- /dev/null +++ b/crates/uplc/test_data/conformance/evaluation/builtin/interleaving/iteWrongCondTypePartiallyApplied/iteWrongCondTypePartiallyApplied.uplc.expected @@ -0,0 +1,4 @@ +(program 1.0.0 [ + [ (force (builtin ifThenElse)) (con string "11 <= 22") ] + (con string "\172(11 <= 22)") +]) \ No newline at end of file diff --git a/crates/uplc/test_data/conformance/evaluation/term/unlifting-unsat/unlifting-unsat.uplc b/crates/uplc/test_data/conformance/evaluation/term/unlifting-unsat/unlifting-unsat.uplc new file mode 100644 index 00000000..ce2efb7b --- /dev/null +++ b/crates/uplc/test_data/conformance/evaluation/term/unlifting-unsat/unlifting-unsat.uplc @@ -0,0 +1,2 @@ +-- ill-typed but does not fail at runtime because the builtin application is not saturated. +(program 1.0.0 [(builtin addInteger) (con unit ())]) \ No newline at end of file diff --git a/crates/uplc/test_data/conformance/evaluation/term/unlifting-unsat/unlifting-unsat.uplc.expected b/crates/uplc/test_data/conformance/evaluation/term/unlifting-unsat/unlifting-unsat.uplc.expected new file mode 100644 index 00000000..57d2ddd4 --- /dev/null +++ b/crates/uplc/test_data/conformance/evaluation/term/unlifting-unsat/unlifting-unsat.uplc.expected @@ -0,0 +1 @@ +(program 1.0.0 [ (builtin addInteger) (con unit ()) ]) \ No newline at end of file From df992cba67841c944aab1719c4f3aca02e2ac857 Mon Sep 17 00:00:00 2001 From: rvcas Date: Fri, 17 Nov 2023 00:12:48 -0500 Subject: [PATCH 02/25] chore: remove check_type --- crates/uplc/src/machine/runtime.rs | 202 ++++------------------------- 1 file changed, 25 insertions(+), 177 deletions(-) diff --git a/crates/uplc/src/machine/runtime.rs b/crates/uplc/src/machine/runtime.rs index 2f53c6cd..66de61bc 100644 --- a/crates/uplc/src/machine/runtime.rs +++ b/crates/uplc/src/machine/runtime.rs @@ -262,168 +262,6 @@ impl DefaultFunction { } } - pub fn check_type(&self, arg: &Value, args: &[Value]) -> Result<(), Error> { - match self { - DefaultFunction::AddInteger => arg.expect_type(Type::Integer), - DefaultFunction::SubtractInteger => arg.expect_type(Type::Integer), - DefaultFunction::MultiplyInteger => arg.expect_type(Type::Integer), - DefaultFunction::DivideInteger => arg.expect_type(Type::Integer), - DefaultFunction::QuotientInteger => arg.expect_type(Type::Integer), - DefaultFunction::RemainderInteger => arg.expect_type(Type::Integer), - DefaultFunction::ModInteger => arg.expect_type(Type::Integer), - DefaultFunction::EqualsInteger => arg.expect_type(Type::Integer), - DefaultFunction::LessThanInteger => arg.expect_type(Type::Integer), - DefaultFunction::LessThanEqualsInteger => arg.expect_type(Type::Integer), - DefaultFunction::AppendByteString => arg.expect_type(Type::ByteString), - DefaultFunction::ConsByteString => { - if args.is_empty() { - arg.expect_type(Type::Integer) - } else { - arg.expect_type(Type::ByteString) - } - } - DefaultFunction::SliceByteString => { - if args.len() < 2 { - arg.expect_type(Type::Integer) - } else { - arg.expect_type(Type::ByteString) - } - } - DefaultFunction::LengthOfByteString => arg.expect_type(Type::ByteString), - DefaultFunction::IndexByteString => { - if args.is_empty() { - arg.expect_type(Type::ByteString) - } else { - arg.expect_type(Type::Integer) - } - } - DefaultFunction::EqualsByteString => arg.expect_type(Type::ByteString), - DefaultFunction::LessThanByteString => arg.expect_type(Type::ByteString), - DefaultFunction::LessThanEqualsByteString => arg.expect_type(Type::ByteString), - DefaultFunction::Sha2_256 => arg.expect_type(Type::ByteString), - DefaultFunction::Sha3_256 => arg.expect_type(Type::ByteString), - DefaultFunction::Blake2b_224 => arg.expect_type(Type::ByteString), - DefaultFunction::Blake2b_256 => arg.expect_type(Type::ByteString), - DefaultFunction::Keccak_256 => arg.expect_type(Type::ByteString), - DefaultFunction::VerifyEd25519Signature => arg.expect_type(Type::ByteString), - DefaultFunction::VerifyEcdsaSecp256k1Signature => arg.expect_type(Type::ByteString), - DefaultFunction::VerifySchnorrSecp256k1Signature => arg.expect_type(Type::ByteString), - DefaultFunction::AppendString => arg.expect_type(Type::String), - DefaultFunction::EqualsString => arg.expect_type(Type::String), - DefaultFunction::EncodeUtf8 => arg.expect_type(Type::String), - DefaultFunction::DecodeUtf8 => arg.expect_type(Type::ByteString), - DefaultFunction::IfThenElse => { - if args.is_empty() { - arg.expect_type(Type::Bool) - } else { - Ok(()) - } - } - DefaultFunction::ChooseUnit => { - if args.is_empty() { - arg.expect_type(Type::Unit) - } else { - Ok(()) - } - } - DefaultFunction::Trace => { - if args.is_empty() { - arg.expect_type(Type::String) - } else { - Ok(()) - } - } - DefaultFunction::FstPair => arg.expect_pair(), - DefaultFunction::SndPair => arg.expect_pair(), - DefaultFunction::ChooseList => { - if args.is_empty() { - arg.expect_list() - } else { - Ok(()) - } - } - DefaultFunction::MkCons => { - if args.is_empty() { - Ok(()) - } else { - let first = &args[0]; - - arg.expect_type(Type::List(Rc::new(first.try_into()?))) - } - } - DefaultFunction::HeadList => arg.expect_list(), - DefaultFunction::TailList => arg.expect_list(), - DefaultFunction::NullList => arg.expect_list(), - DefaultFunction::ChooseData => { - if args.is_empty() { - arg.expect_type(Type::Data) - } else { - Ok(()) - } - } - DefaultFunction::ConstrData => { - if args.is_empty() { - arg.expect_type(Type::Integer) - } else { - arg.expect_type(Type::List(Rc::new(Type::Data))) - } - } - DefaultFunction::MapData => arg.expect_type(Type::List(Rc::new(Type::Pair( - Rc::new(Type::Data), - Rc::new(Type::Data), - )))), - DefaultFunction::ListData => arg.expect_type(Type::List(Rc::new(Type::Data))), - DefaultFunction::IData => arg.expect_type(Type::Integer), - DefaultFunction::BData => arg.expect_type(Type::ByteString), - DefaultFunction::UnConstrData => arg.expect_type(Type::Data), - DefaultFunction::UnMapData => arg.expect_type(Type::Data), - DefaultFunction::UnListData => arg.expect_type(Type::Data), - DefaultFunction::UnIData => arg.expect_type(Type::Data), - DefaultFunction::UnBData => arg.expect_type(Type::Data), - DefaultFunction::EqualsData => arg.expect_type(Type::Data), - DefaultFunction::SerialiseData => arg.expect_type(Type::Data), - DefaultFunction::MkPairData => arg.expect_type(Type::Data), - DefaultFunction::MkNilData => arg.expect_type(Type::Unit), - DefaultFunction::MkNilPairData => arg.expect_type(Type::Unit), - - DefaultFunction::Bls12_381_G1_Add => arg.expect_type(Type::Bls12_381G1Element), - DefaultFunction::Bls12_381_G1_Neg => arg.expect_type(Type::Bls12_381G1Element), - DefaultFunction::Bls12_381_G1_ScalarMul => { - if args.is_empty() { - arg.expect_type(Type::Integer) - } else { - arg.expect_type(Type::Bls12_381G1Element) - } - } - DefaultFunction::Bls12_381_G1_Equal => arg.expect_type(Type::Bls12_381G1Element), - DefaultFunction::Bls12_381_G1_Compress => arg.expect_type(Type::Bls12_381G1Element), - DefaultFunction::Bls12_381_G1_Uncompress => arg.expect_type(Type::ByteString), - DefaultFunction::Bls12_381_G1_HashToGroup => arg.expect_type(Type::ByteString), - DefaultFunction::Bls12_381_G2_Add => arg.expect_type(Type::Bls12_381G2Element), - DefaultFunction::Bls12_381_G2_Neg => arg.expect_type(Type::Bls12_381G2Element), - DefaultFunction::Bls12_381_G2_ScalarMul => { - if args.is_empty() { - arg.expect_type(Type::Integer) - } else { - arg.expect_type(Type::Bls12_381G2Element) - } - } - DefaultFunction::Bls12_381_G2_Equal => arg.expect_type(Type::Bls12_381G2Element), - DefaultFunction::Bls12_381_G2_Compress => arg.expect_type(Type::Bls12_381G2Element), - DefaultFunction::Bls12_381_G2_Uncompress => arg.expect_type(Type::ByteString), - DefaultFunction::Bls12_381_G2_HashToGroup => arg.expect_type(Type::ByteString), - DefaultFunction::Bls12_381_MillerLoop => { - if args.is_empty() { - arg.expect_type(Type::Bls12_381G1Element) - } else { - arg.expect_type(Type::Bls12_381G2Element) - } - } - DefaultFunction::Bls12_381_MulMlResult => arg.expect_type(Type::Bls12_381MlResult), - DefaultFunction::Bls12_381_FinalVerify => arg.expect_type(Type::Bls12_381MlResult), - } - } - // This should be safe because we've already checked // the types of the args as they were pushed. Although // the unreachables look ugly, it's the reality of the situation. @@ -893,15 +731,14 @@ impl DefaultFunction { Ok(value) } DefaultFunction::ChooseData => { - let con = args[0].unwrap_constant()?; + let con = args[0].unwrap_data()?; match con { - Constant::Data(PlutusData::Constr(_)) => Ok(args[1].clone()), - Constant::Data(PlutusData::Map(_)) => Ok(args[2].clone()), - Constant::Data(PlutusData::Array(_)) => Ok(args[3].clone()), - Constant::Data(PlutusData::BigInt(_)) => Ok(args[4].clone()), - Constant::Data(PlutusData::BoundedBytes(_)) => Ok(args[5].clone()), - _ => unreachable!(), + PlutusData::Constr(_) => Ok(args[1].clone()), + PlutusData::Map(_) => Ok(args[2].clone()), + PlutusData::Array(_) => Ok(args[3].clone()), + PlutusData::BigInt(_) => Ok(args[4].clone()), + PlutusData::BoundedBytes(_) => Ok(args[5].clone()), } } DefaultFunction::ConstrData => { @@ -929,7 +766,17 @@ impl DefaultFunction { Ok(value) } DefaultFunction::MapData => { - let (_, list) = args[0].unwrap_list()?; + let (r#type, list) = args[0].unwrap_list()?; + + if *r#type != Type::Pair(Rc::new(Type::Data), Rc::new(Type::Data)) { + return Err(Error::TypeMismatch( + Type::List(Rc::new(Type::Pair( + Rc::new(Type::Data), + Rc::new(Type::Data), + ))), + r#type.clone(), + )); + } let mut map = Vec::new(); @@ -938,12 +785,13 @@ impl DefaultFunction { unreachable!() }; - match (left.as_ref(), right.as_ref()) { - (Constant::Data(key), Constant::Data(value)) => { - map.push((key.clone(), value.clone())); - } - _ => unreachable!(), - } + let (Constant::Data(key), Constant::Data(value)) = + (left.as_ref(), right.as_ref()) + else { + unreachable!() + }; + + map.push((key.clone(), value.clone())); } let value = Value::data(PlutusData::Map(map.into())); @@ -951,7 +799,7 @@ impl DefaultFunction { Ok(value) } DefaultFunction::ListData => { - let (_, list) = args[0].unwrap_list()?; + let list = args[0].unwrap_data_list()?; let data_list: Vec = list .iter() From 0382e5ce122845bb8bc936f371fe6b2c5ab15263 Mon Sep 17 00:00:00 2001 From: rvcas Date: Fri, 17 Nov 2023 18:41:28 -0500 Subject: [PATCH 03/25] chore: this comment doesn't make sense --- crates/uplc/src/machine/runtime.rs | 3 --- 1 file changed, 3 deletions(-) diff --git a/crates/uplc/src/machine/runtime.rs b/crates/uplc/src/machine/runtime.rs index 66de61bc..8ae2e292 100644 --- a/crates/uplc/src/machine/runtime.rs +++ b/crates/uplc/src/machine/runtime.rs @@ -262,9 +262,6 @@ impl DefaultFunction { } } - // This should be safe because we've already checked - // the types of the args as they were pushed. Although - // the unreachables look ugly, it's the reality of the situation. pub fn call( &self, semantics: BuiltinSemantics, From 1567e42875b2f5a4b68485ec17a4d13b50e056fe Mon Sep 17 00:00:00 2001 From: microproofs Date: Fri, 17 Nov 2023 19:52:03 -0500 Subject: [PATCH 04/25] chore: fill in machine todos and cost model for case and constr This allows for several more tests to pass **Had to remove case-7 since it was incorrectly passing before** --- crates/uplc/src/machine.rs | 7 +++++-- crates/uplc/src/machine/cost_model.rs | 8 ++++---- crates/uplc/src/machine/error.rs | 5 +++++ crates/uplc/src/parser.rs | 2 +- .../conformance/evaluation/term/case/case-1/case-1.uplc | 4 ++++ .../evaluation/term/case/case-1/case-1.uplc.expected | 1 + .../conformance/evaluation/term/case/case-2/case-2.uplc | 4 ++++ .../evaluation/term/case/case-2/case-2.uplc.expected | 1 + .../conformance/evaluation/term/case/case-3/case-3.uplc | 4 ++++ .../evaluation/term/case/case-3/case-3.uplc.expected | 1 + .../conformance/evaluation/term/case/case-4/case-4.uplc | 4 ++++ .../evaluation/term/case/case-4/case-4.uplc.expected | 1 + .../conformance/evaluation/term/case/case-5/case-5.uplc | 4 ++++ .../evaluation/term/case/case-5/case-5.uplc.expected | 1 + .../conformance/evaluation/term/case/case-7/case-7.uplc | 4 ---- .../evaluation/term/case/case-7/case-7.uplc.expected | 1 - .../conformance/evaluation/term/case/case-8/case-8.uplc | 4 ++++ .../evaluation/term/case/case-8/case-8.uplc.expected | 1 + .../conformance/evaluation/term/case/case-9/case-9.uplc | 4 ++++ .../evaluation/term/case/case-9/case-9.uplc.expected | 1 + .../evaluation/term/constr/constr-1/constr-1.uplc | 4 ++++ .../term/constr/constr-1/constr-1.uplc.expected | 1 + .../evaluation/term/constr/constr-2/constr-2.uplc | 4 ++++ .../term/constr/constr-2/constr-2.uplc.expected | 1 + 24 files changed, 60 insertions(+), 12 deletions(-) create mode 100644 crates/uplc/test_data/conformance/evaluation/term/case/case-1/case-1.uplc create mode 100644 crates/uplc/test_data/conformance/evaluation/term/case/case-1/case-1.uplc.expected create mode 100644 crates/uplc/test_data/conformance/evaluation/term/case/case-2/case-2.uplc create mode 100644 crates/uplc/test_data/conformance/evaluation/term/case/case-2/case-2.uplc.expected create mode 100644 crates/uplc/test_data/conformance/evaluation/term/case/case-3/case-3.uplc create mode 100644 crates/uplc/test_data/conformance/evaluation/term/case/case-3/case-3.uplc.expected create mode 100644 crates/uplc/test_data/conformance/evaluation/term/case/case-4/case-4.uplc create mode 100644 crates/uplc/test_data/conformance/evaluation/term/case/case-4/case-4.uplc.expected create mode 100644 crates/uplc/test_data/conformance/evaluation/term/case/case-5/case-5.uplc create mode 100644 crates/uplc/test_data/conformance/evaluation/term/case/case-5/case-5.uplc.expected delete mode 100644 crates/uplc/test_data/conformance/evaluation/term/case/case-7/case-7.uplc delete mode 100644 crates/uplc/test_data/conformance/evaluation/term/case/case-7/case-7.uplc.expected create mode 100644 crates/uplc/test_data/conformance/evaluation/term/case/case-8/case-8.uplc create mode 100644 crates/uplc/test_data/conformance/evaluation/term/case/case-8/case-8.uplc.expected create mode 100644 crates/uplc/test_data/conformance/evaluation/term/case/case-9/case-9.uplc create mode 100644 crates/uplc/test_data/conformance/evaluation/term/case/case-9/case-9.uplc.expected create mode 100644 crates/uplc/test_data/conformance/evaluation/term/constr/constr-1/constr-1.uplc create mode 100644 crates/uplc/test_data/conformance/evaluation/term/constr/constr-1/constr-1.uplc.expected create mode 100644 crates/uplc/test_data/conformance/evaluation/term/constr/constr-2/constr-2.uplc create mode 100644 crates/uplc/test_data/conformance/evaluation/term/constr/constr-2/constr-2.uplc.expected diff --git a/crates/uplc/src/machine.rs b/crates/uplc/src/machine.rs index 81cd44ae..f6598d9a 100644 --- a/crates/uplc/src/machine.rs +++ b/crates/uplc/src/machine.rs @@ -240,9 +240,12 @@ impl Machine { env, t.clone(), )), - None => todo!(), + None => Err(Error::MissingCaseBranch( + branches, + Value::Constr { tag, fields }, + )), }, - _ => todo!("return a proper evaluation error"), + v => Err(Error::NonConstrScrutinized(v)), }, } } diff --git a/crates/uplc/src/machine/cost_model.rs b/crates/uplc/src/machine/cost_model.rs index 6b900258..2701fc0c 100644 --- a/crates/uplc/src/machine/cost_model.rs +++ b/crates/uplc/src/machine/cost_model.rs @@ -186,12 +186,12 @@ impl Default for MachineCosts { }, // Placeholder values constr: ExBudget { - mem: 30000000000, - cpu: 30000000000, + mem: 100, + cpu: 23000, }, case: ExBudget { - mem: 30000000000, - cpu: 30000000000, + mem: 100, + cpu: 23000, }, } } diff --git a/crates/uplc/src/machine/error.rs b/crates/uplc/src/machine/error.rs index 468cb518..bac398ca 100644 --- a/crates/uplc/src/machine/error.rs +++ b/crates/uplc/src/machine/error.rs @@ -20,6 +20,10 @@ pub enum Error { NonPolymorphicInstantiation(Value), #[error("Attempted to apply a non-function:\n\n{0:#?} to argument:\n\n{1:#?}")] NonFunctionalApplication(Value, Value), + #[error("Attempted to case a non-const:\n\n{0:#?}")] + NonConstrScrutinized(Value), + #[error("Cases: {0:#?}\n\n are missing branch for constr:\n\n{1:#?}")] + MissingCaseBranch(Vec>, Value), #[error("Type mismatch expected '{0}' got '{1}'")] TypeMismatch(Type, Type), #[error("Type mismatch expected '(list a)' got '{0}'")] @@ -36,6 +40,7 @@ pub enum Error { NotAConstant(Value), #[error("The evaluation never reached a final state")] MachineNeverReachedDone, + #[error("Decoding utf8")] Utf8(#[from] FromUtf8Error), #[error("Out of Bounds\n\nindex: {}\nbytestring: {}\npossible: 0 - {}", .0, hex::encode(.1), .1.len() - 1)] diff --git a/crates/uplc/src/parser.rs b/crates/uplc/src/parser.rs index 79451837..29adc8d7 100644 --- a/crates/uplc/src/parser.rs +++ b/crates/uplc/src/parser.rs @@ -145,7 +145,7 @@ peg::parser! { } rule case(interner: &mut Interner) -> Term - = "(" _* "case" _+ constr:term(interner) _* branches:(t:term(interner) _* { t })+ _* ")" { + = "(" _* "case" _+ constr:term(interner) _* branches:(t:term(interner) _* { t })* _* ")" { Term::Case { constr: constr.into(), branches } } diff --git a/crates/uplc/test_data/conformance/evaluation/term/case/case-1/case-1.uplc b/crates/uplc/test_data/conformance/evaluation/term/case/case-1/case-1.uplc new file mode 100644 index 00000000..a08de31c --- /dev/null +++ b/crates/uplc/test_data/conformance/evaluation/term/case/case-1/case-1.uplc @@ -0,0 +1,4 @@ +-- select first branch +(program 1.1.0 + (case (constr 0 (con integer 0)) (lam x (con integer 1)) (lam x (con integer 2))) +) diff --git a/crates/uplc/test_data/conformance/evaluation/term/case/case-1/case-1.uplc.expected b/crates/uplc/test_data/conformance/evaluation/term/case/case-1/case-1.uplc.expected new file mode 100644 index 00000000..c897a619 --- /dev/null +++ b/crates/uplc/test_data/conformance/evaluation/term/case/case-1/case-1.uplc.expected @@ -0,0 +1 @@ +(program 1.1.0 (con integer 1)) \ No newline at end of file diff --git a/crates/uplc/test_data/conformance/evaluation/term/case/case-2/case-2.uplc b/crates/uplc/test_data/conformance/evaluation/term/case/case-2/case-2.uplc new file mode 100644 index 00000000..b9861165 --- /dev/null +++ b/crates/uplc/test_data/conformance/evaluation/term/case/case-2/case-2.uplc @@ -0,0 +1,4 @@ +-- select second branch +(program 1.1.0 + (case (constr 1 (con integer 0)) (lam x (con integer 1)) (lam x (con integer 2))) +) diff --git a/crates/uplc/test_data/conformance/evaluation/term/case/case-2/case-2.uplc.expected b/crates/uplc/test_data/conformance/evaluation/term/case/case-2/case-2.uplc.expected new file mode 100644 index 00000000..2b513f80 --- /dev/null +++ b/crates/uplc/test_data/conformance/evaluation/term/case/case-2/case-2.uplc.expected @@ -0,0 +1 @@ +(program 1.1.0 (con integer 2)) \ No newline at end of file diff --git a/crates/uplc/test_data/conformance/evaluation/term/case/case-3/case-3.uplc b/crates/uplc/test_data/conformance/evaluation/term/case/case-3/case-3.uplc new file mode 100644 index 00000000..a4399f95 --- /dev/null +++ b/crates/uplc/test_data/conformance/evaluation/term/case/case-3/case-3.uplc @@ -0,0 +1,4 @@ +-- select first branch and do computation with the args +(program 1.1.0 + (case (constr 0 (con integer 3) (con integer 2)) (lam x (lam y [(builtin addInteger) x y])) (lam x (lam y [(builtin subtractInteger) x y]))) +) diff --git a/crates/uplc/test_data/conformance/evaluation/term/case/case-3/case-3.uplc.expected b/crates/uplc/test_data/conformance/evaluation/term/case/case-3/case-3.uplc.expected new file mode 100644 index 00000000..307ccca4 --- /dev/null +++ b/crates/uplc/test_data/conformance/evaluation/term/case/case-3/case-3.uplc.expected @@ -0,0 +1 @@ +(program 1.1.0 (con integer 5)) \ No newline at end of file diff --git a/crates/uplc/test_data/conformance/evaluation/term/case/case-4/case-4.uplc b/crates/uplc/test_data/conformance/evaluation/term/case/case-4/case-4.uplc new file mode 100644 index 00000000..e471f4ec --- /dev/null +++ b/crates/uplc/test_data/conformance/evaluation/term/case/case-4/case-4.uplc @@ -0,0 +1,4 @@ +-- select second branch and do computation with the args +(program 1.1.0 + (case (constr 1 (con integer 3) (con integer 2)) (lam x (lam y [(builtin addInteger) x y])) (lam x (lam y [(builtin subtractInteger) x y]))) +) diff --git a/crates/uplc/test_data/conformance/evaluation/term/case/case-4/case-4.uplc.expected b/crates/uplc/test_data/conformance/evaluation/term/case/case-4/case-4.uplc.expected new file mode 100644 index 00000000..c897a619 --- /dev/null +++ b/crates/uplc/test_data/conformance/evaluation/term/case/case-4/case-4.uplc.expected @@ -0,0 +1 @@ +(program 1.1.0 (con integer 1)) \ No newline at end of file diff --git a/crates/uplc/test_data/conformance/evaluation/term/case/case-5/case-5.uplc b/crates/uplc/test_data/conformance/evaluation/term/case/case-5/case-5.uplc new file mode 100644 index 00000000..5478b918 --- /dev/null +++ b/crates/uplc/test_data/conformance/evaluation/term/case/case-5/case-5.uplc @@ -0,0 +1,4 @@ +-- case of non-constr +(program 1.1.0 + (case (con integer 1) (lam x x) (lam x x)) +) diff --git a/crates/uplc/test_data/conformance/evaluation/term/case/case-5/case-5.uplc.expected b/crates/uplc/test_data/conformance/evaluation/term/case/case-5/case-5.uplc.expected new file mode 100644 index 00000000..ccc477ff --- /dev/null +++ b/crates/uplc/test_data/conformance/evaluation/term/case/case-5/case-5.uplc.expected @@ -0,0 +1 @@ +evaluation failure \ No newline at end of file diff --git a/crates/uplc/test_data/conformance/evaluation/term/case/case-7/case-7.uplc b/crates/uplc/test_data/conformance/evaluation/term/case/case-7/case-7.uplc deleted file mode 100644 index 94153d58..00000000 --- a/crates/uplc/test_data/conformance/evaluation/term/case/case-7/case-7.uplc +++ /dev/null @@ -1,4 +0,0 @@ --- case can't be used before 1.1.0 -(program 1.0.0 - (case (con integer 1)) -) diff --git a/crates/uplc/test_data/conformance/evaluation/term/case/case-7/case-7.uplc.expected b/crates/uplc/test_data/conformance/evaluation/term/case/case-7/case-7.uplc.expected deleted file mode 100644 index fd569489..00000000 --- a/crates/uplc/test_data/conformance/evaluation/term/case/case-7/case-7.uplc.expected +++ /dev/null @@ -1 +0,0 @@ -parse error \ No newline at end of file diff --git a/crates/uplc/test_data/conformance/evaluation/term/case/case-8/case-8.uplc b/crates/uplc/test_data/conformance/evaluation/term/case/case-8/case-8.uplc new file mode 100644 index 00000000..5aae469b --- /dev/null +++ b/crates/uplc/test_data/conformance/evaluation/term/case/case-8/case-8.uplc @@ -0,0 +1,4 @@ +-- nullary case +(program 1.1.0 + (case (constr 0) (con integer 1) (con integer 2)) +) diff --git a/crates/uplc/test_data/conformance/evaluation/term/case/case-8/case-8.uplc.expected b/crates/uplc/test_data/conformance/evaluation/term/case/case-8/case-8.uplc.expected new file mode 100644 index 00000000..c897a619 --- /dev/null +++ b/crates/uplc/test_data/conformance/evaluation/term/case/case-8/case-8.uplc.expected @@ -0,0 +1 @@ +(program 1.1.0 (con integer 1)) \ No newline at end of file diff --git a/crates/uplc/test_data/conformance/evaluation/term/case/case-9/case-9.uplc b/crates/uplc/test_data/conformance/evaluation/term/case/case-9/case-9.uplc new file mode 100644 index 00000000..73b0719b --- /dev/null +++ b/crates/uplc/test_data/conformance/evaluation/term/case/case-9/case-9.uplc @@ -0,0 +1,4 @@ +-- empty case, aka -XEmptyCase +(program 1.1.0 + (case (constr 0)) +) diff --git a/crates/uplc/test_data/conformance/evaluation/term/case/case-9/case-9.uplc.expected b/crates/uplc/test_data/conformance/evaluation/term/case/case-9/case-9.uplc.expected new file mode 100644 index 00000000..ccc477ff --- /dev/null +++ b/crates/uplc/test_data/conformance/evaluation/term/case/case-9/case-9.uplc.expected @@ -0,0 +1 @@ +evaluation failure \ No newline at end of file diff --git a/crates/uplc/test_data/conformance/evaluation/term/constr/constr-1/constr-1.uplc b/crates/uplc/test_data/conformance/evaluation/term/constr/constr-1/constr-1.uplc new file mode 100644 index 00000000..9c413fa7 --- /dev/null +++ b/crates/uplc/test_data/conformance/evaluation/term/constr/constr-1/constr-1.uplc @@ -0,0 +1,4 @@ +-- empty constr +(program 1.1.0 + (constr 0 ) +) diff --git a/crates/uplc/test_data/conformance/evaluation/term/constr/constr-1/constr-1.uplc.expected b/crates/uplc/test_data/conformance/evaluation/term/constr/constr-1/constr-1.uplc.expected new file mode 100644 index 00000000..a65a6aca --- /dev/null +++ b/crates/uplc/test_data/conformance/evaluation/term/constr/constr-1/constr-1.uplc.expected @@ -0,0 +1 @@ +(program 1.1.0 (constr 0)) \ No newline at end of file diff --git a/crates/uplc/test_data/conformance/evaluation/term/constr/constr-2/constr-2.uplc b/crates/uplc/test_data/conformance/evaluation/term/constr/constr-2/constr-2.uplc new file mode 100644 index 00000000..a27c1c66 --- /dev/null +++ b/crates/uplc/test_data/conformance/evaluation/term/constr/constr-2/constr-2.uplc @@ -0,0 +1,4 @@ +-- constr with an argument +(program 1.1.0 + (constr 0 (con integer 1)) +) diff --git a/crates/uplc/test_data/conformance/evaluation/term/constr/constr-2/constr-2.uplc.expected b/crates/uplc/test_data/conformance/evaluation/term/constr/constr-2/constr-2.uplc.expected new file mode 100644 index 00000000..fafd655d --- /dev/null +++ b/crates/uplc/test_data/conformance/evaluation/term/constr/constr-2/constr-2.uplc.expected @@ -0,0 +1 @@ +(program 1.1.0 (constr 0 (con integer 1))) \ No newline at end of file From 6869f730335c361f83558e77986499d2a172b0e4 Mon Sep 17 00:00:00 2001 From: rvcas Date: Sat, 18 Nov 2023 16:48:28 -0500 Subject: [PATCH 05/25] fix: sequence formatting when not top level --- crates/aiken-lang/src/format.rs | 85 ++++++++++++------- crates/aiken-lang/src/tests/format.rs | 25 ++++++ .../format_logic_op_with_code_block.snap | 11 +++ crates/aiken-lang/src/tipo/error.rs | 2 +- crates/aiken-lang/src/tipo/expr.rs | 4 +- 5 files changed, 95 insertions(+), 32 deletions(-) create mode 100644 crates/aiken-lang/src/tests/snapshots/format_logic_op_with_code_block.snap diff --git a/crates/aiken-lang/src/format.rs b/crates/aiken-lang/src/format.rs index 4537c25d..d74c6ad8 100644 --- a/crates/aiken-lang/src/format.rs +++ b/crates/aiken-lang/src/format.rs @@ -516,7 +516,7 @@ impl<'comments> Formatter<'comments> { .group(); // Format body - let body = self.expr(body); + let body = self.expr(body, true); // Add any trailing comments let body = match printed_comments(self.pop_comments(end_location), false) { @@ -609,8 +609,10 @@ impl<'comments> Formatter<'comments> { ) -> Document<'a> { let args = wrap_args(args.iter().map(|e| (self.fn_arg(e), false))).group(); let body = match body { - UntypedExpr::Trace { .. } | UntypedExpr::When { .. } => self.expr(body).force_break(), - _ => self.expr(body), + UntypedExpr::Trace { .. } | UntypedExpr::When { .. } => { + self.expr(body, false).force_break() + } + _ => self.expr(body, false), }; let header = "fn".to_doc().append(args); @@ -634,15 +636,19 @@ impl<'comments> Formatter<'comments> { fn sequence<'a>(&mut self, expressions: &'a [UntypedExpr]) -> Document<'a> { let count = expressions.len(); let mut documents = Vec::with_capacity(count * 2); + for (i, expression) in expressions.iter().enumerate() { let preceding_newline = self.pop_empty_lines(expression.start_byte_index()); + if i != 0 && preceding_newline { documents.push(lines(2)); } else if i != 0 { documents.push(lines(1)); } - documents.push(self.expr(expression).group()); + + documents.push(self.expr(expression, false).group()); } + documents.to_doc().force_break() } @@ -782,7 +788,7 @@ impl<'comments> Formatter<'comments> { } } - pub fn expr<'a>(&mut self, expr: &'a UntypedExpr) -> Document<'a> { + pub fn expr<'a>(&mut self, expr: &'a UntypedExpr, is_top_level: bool) -> Document<'a> { let comments = self.pop_comments(expr.start_byte_index()); let document = match expr { @@ -821,7 +827,18 @@ impl<'comments> Formatter<'comments> { UntypedExpr::String { value, .. } => self.string(value), - UntypedExpr::Sequence { expressions, .. } => self.sequence(expressions), + UntypedExpr::Sequence { expressions, .. } => { + let sequence = self.sequence(expressions); + + if is_top_level { + sequence + } else { + "{".to_doc() + .append(line().append(sequence).nest(INDENT).group()) + .append(line()) + .append("}") + } + } UntypedExpr::Var { name, .. } if name.contains(CAPTURE_VARIABLE) => "_".to_doc(), @@ -878,7 +895,10 @@ impl<'comments> Formatter<'comments> { UntypedExpr::FieldAccess { label, container, .. - } => self.expr(container).append(".").append(label.as_str()), + } => self + .expr(container, false) + .append(".") + .append(label.as_str()), UntypedExpr::RecordUpdate { constructor, @@ -893,7 +913,7 @@ impl<'comments> Formatter<'comments> { UntypedExpr::TupleIndex { index, tuple, .. } => { let suffix = Ordinal(*index + 1).suffix().to_doc(); - self.expr(tuple) + self.expr(tuple, false) .append(".".to_doc()) .append((index + 1).to_doc()) .append(suffix) @@ -953,7 +973,7 @@ impl<'comments> Formatter<'comments> { } else { line() }) - .append(self.expr(then)), + .append(self.expr(then, false)), } } @@ -1027,7 +1047,7 @@ impl<'comments> Formatter<'comments> { false }; - self.expr(fun) + self.expr(fun, false) .append(wrap_args( args.iter() .map(|a| (self.call_arg(a, needs_curly), needs_curly)), @@ -1051,7 +1071,7 @@ impl<'comments> Formatter<'comments> { let else_begin = line().append("} else {"); - let else_body = line().append(self.expr(final_else)).nest(INDENT); + let else_body = line().append(self.expr(final_else, false)).nest(INDENT); let else_end = line().append("}"); @@ -1072,7 +1092,7 @@ impl<'comments> Formatter<'comments> { .append(break_("{", " {")) .group(); - let if_body = line().append(self.expr(&branch.body)).nest(INDENT); + let if_body = line().append(self.expr(&branch.body, false)).nest(INDENT); if_begin.append(if_body) } @@ -1110,8 +1130,8 @@ impl<'comments> Formatter<'comments> { args: &'a [UntypedRecordUpdateArg], ) -> Document<'a> { use std::iter::once; - let constructor_doc = self.expr(constructor); - let spread_doc = "..".to_doc().append(self.expr(&spread.base)); + let constructor_doc = self.expr(constructor, false); + let spread_doc = "..".to_doc().append(self.expr(&spread.base, false)); let arg_docs = args.iter().map(|a| (self.record_update_arg(a), true)); let all_arg_docs = once((spread_doc, true)).chain(arg_docs); constructor_doc.append(wrap_args(all_arg_docs)).group() @@ -1128,8 +1148,8 @@ impl<'comments> Formatter<'comments> { let left_precedence = left.binop_precedence(); let right_precedence = right.binop_precedence(); - let left = self.expr(left); - let right = self.expr(right); + let left = self.expr(left, false); + let right = self.expr(right, false); self.operator_side(left, precedence, left_precedence) .append(" ") @@ -1161,7 +1181,9 @@ impl<'comments> Formatter<'comments> { .append( line() .append(join( - expressions.iter().map(|expression| self.expr(expression)), + expressions + .iter() + .map(|expression| self.expr(expression, false)), ",".to_doc().append(line()), )) .nest(INDENT) @@ -1241,10 +1263,10 @@ impl<'comments> Formatter<'comments> { if hole_in_first_position && args.len() == 1 { // x |> fun(_) - self.expr(fun) + self.expr(fun, false) } else if hole_in_first_position { // x |> fun(_, 2, 3) - self.expr(fun).append( + self.expr(fun, false).append( wrap_args( args.iter() .skip(1) @@ -1254,7 +1276,7 @@ impl<'comments> Formatter<'comments> { ) } else { // x |> fun(1, _, 3) - self.expr(fun) + self.expr(fun, false) .append(wrap_args(args.iter().map(|a| (self.call_arg(a, false), false))).group()) } } @@ -1267,14 +1289,14 @@ impl<'comments> Formatter<'comments> { .. } => match args.as_slice() { [first, second] if is_breakable_expr(&second.value) && first.is_capture_hole() => { - self.expr(fun) + self.expr(fun, false) .append("(_, ") .append(self.call_arg(second, false)) .append(")") .group() } - _ => self.expr(fun).append( + _ => self.expr(fun, false).append( wrap_args(args.iter().map(|a| (self.call_arg(a, false), false))).group(), ), }, @@ -1556,12 +1578,12 @@ impl<'comments> Formatter<'comments> { | UntypedExpr::Sequence { .. } | UntypedExpr::Assignment { .. } => "{" .to_doc() - .append(line().append(self.expr(expr)).nest(INDENT)) + .append(line().append(self.expr(expr, false)).nest(INDENT)) .append(line()) .append("}") .force_break(), - _ => self.expr(expr), + _ => self.expr(expr, false), } } @@ -1598,18 +1620,21 @@ impl<'comments> Formatter<'comments> { | UntypedExpr::Sequence { .. } | UntypedExpr::Assignment { .. } => " {" .to_doc() - .append(line().append(self.expr(expr)).nest(INDENT).group()) + .append(line().append(self.expr(expr, true)).nest(INDENT).group()) .append(line()) .append("}") .force_break(), UntypedExpr::Fn { .. } | UntypedExpr::List { .. } => { - line().append(self.expr(expr)).nest(INDENT).group() + line().append(self.expr(expr, false)).nest(INDENT).group() } - UntypedExpr::When { .. } => line().append(self.expr(expr)).nest(INDENT).group(), + UntypedExpr::When { .. } => line().append(self.expr(expr, false)).nest(INDENT).group(), - _ => break_("", " ").append(self.expr(expr)).nest(INDENT).group(), + _ => break_("", " ") + .append(self.expr(expr, false)) + .nest(INDENT) + .group(), } } @@ -1647,7 +1672,7 @@ impl<'comments> Formatter<'comments> { || break_(",", ", ") }; let elements_document = join(elements.iter().map(|e| self.wrap_expr(e)), comma()); - let tail = tail.map(|e| self.expr(e)); + let tail = tail.map(|e| self.expr(e, false)); list(elements_document, elements.len(), tail) } @@ -1771,7 +1796,7 @@ impl<'comments> Formatter<'comments> { fn wrap_unary_op<'a>(&mut self, expr: &'a UntypedExpr) -> Document<'a> { match expr { - UntypedExpr::BinOp { .. } => "(".to_doc().append(self.expr(expr)).append(")"), + UntypedExpr::BinOp { .. } => "(".to_doc().append(self.expr(expr, false)).append(")"), _ => self.wrap_expr(expr), } } diff --git a/crates/aiken-lang/src/tests/format.rs b/crates/aiken-lang/src/tests/format.rs index ed11c321..d796b09c 100644 --- a/crates/aiken-lang/src/tests/format.rs +++ b/crates/aiken-lang/src/tests/format.rs @@ -62,6 +62,31 @@ fn format_if() { ); } +#[test] +fn format_logic_op_with_code_block() { + assert_format!( + r#" + fn foo() { + True || { + let bar = 1 + bar == bar + } + } + "# + ); +} + +#[test] +fn format_grouped_expression() { + assert_format!( + r#" + fn foo() { + y == { x |> f } + } + "# + ); +} + #[test] fn format_validator() { assert_format!( diff --git a/crates/aiken-lang/src/tests/snapshots/format_logic_op_with_code_block.snap b/crates/aiken-lang/src/tests/snapshots/format_logic_op_with_code_block.snap new file mode 100644 index 00000000..1129fb7d --- /dev/null +++ b/crates/aiken-lang/src/tests/snapshots/format_logic_op_with_code_block.snap @@ -0,0 +1,11 @@ +--- +source: crates/aiken-lang/src/tests/format.rs +description: "Code:\n\nfn foo() {\n True || {\n let bar = 1\n bar == bar\n }\n}\n" +--- +fn foo() { + True || { + let bar = 1 + bar == bar + } +} + diff --git a/crates/aiken-lang/src/tipo/error.rs b/crates/aiken-lang/src/tipo/error.rs index d298244a..b2fa7b56 100644 --- a/crates/aiken-lang/src/tipo/error.rs +++ b/crates/aiken-lang/src/tipo/error.rs @@ -1630,7 +1630,7 @@ pub enum UnknownRecordFieldSituation { fn format_suggestion(sample: &UntypedExpr) -> String { Formatter::new() - .expr(sample) + .expr(sample, false) .to_pretty_string(70) .lines() .enumerate() diff --git a/crates/aiken-lang/src/tipo/expr.rs b/crates/aiken-lang/src/tipo/expr.rs index bef14653..681d262d 100644 --- a/crates/aiken-lang/src/tipo/expr.rs +++ b/crates/aiken-lang/src/tipo/expr.rs @@ -426,7 +426,9 @@ impl<'a, 'b> ExprTyper<'a, 'b> { tipo: string(), value: format!( "{} ? False", - format::Formatter::new().expr(&value).to_pretty_string(999) + format::Formatter::new() + .expr(&value, false) + .to_pretty_string(999) ), }; From 7680d336634b3663f7baa4ddd5d027b5375427f4 Mon Sep 17 00:00:00 2001 From: rvcas Date: Sat, 18 Nov 2023 17:09:01 -0500 Subject: [PATCH 06/25] fix: panic in formatter when substracting u8 0 - 1 --- crates/aiken-lang/src/format.rs | 6 ++-- crates/aiken-lang/src/tests/format.rs | 33 +++++++++++++++++++ .../snapshots/format_grouped_expression.snap | 8 +++++ .../format_grouped_expression_2.snap | 8 +++++ .../format_grouped_expression_3.snap | 8 +++++ .../format_grouped_expression_4.snap | 8 +++++ 6 files changed, 68 insertions(+), 3 deletions(-) create mode 100644 crates/aiken-lang/src/tests/snapshots/format_grouped_expression.snap create mode 100644 crates/aiken-lang/src/tests/snapshots/format_grouped_expression_2.snap create mode 100644 crates/aiken-lang/src/tests/snapshots/format_grouped_expression_3.snap create mode 100644 crates/aiken-lang/src/tests/snapshots/format_grouped_expression_4.snap diff --git a/crates/aiken-lang/src/format.rs b/crates/aiken-lang/src/format.rs index d74c6ad8..c163d34b 100644 --- a/crates/aiken-lang/src/format.rs +++ b/crates/aiken-lang/src/format.rs @@ -1146,7 +1146,7 @@ impl<'comments> Formatter<'comments> { let precedence = name.precedence(); let left_precedence = left.binop_precedence(); - let right_precedence = right.binop_precedence(); + let right_precedence = dbg!(right).binop_precedence(); let left = self.expr(left, false); let right = self.expr(right, false); @@ -1155,7 +1155,7 @@ impl<'comments> Formatter<'comments> { .append(" ") .append(name) .append(" ") - .append(self.operator_side(right, precedence, right_precedence - 1)) + .append(self.operator_side(right, precedence, right_precedence.saturating_sub(1))) } pub fn operator_side<'a>(&mut self, doc: Document<'a>, op: u8, side: u8) -> Document<'a> { @@ -1745,7 +1745,7 @@ impl<'comments> Formatter<'comments> { let right = self.clause_guard(right); self.operator_side(left, name_precedence, left_precedence) .append(name) - .append(self.operator_side(right, name_precedence, right_precedence - 1)) + .append(self.operator_side(right, name_precedence, right_precedence.saturating_sub(1))) } fn clause_guard<'a>(&mut self, clause_guard: &'a UntypedClauseGuard) -> Document<'a> { diff --git a/crates/aiken-lang/src/tests/format.rs b/crates/aiken-lang/src/tests/format.rs index d796b09c..9c9c12cb 100644 --- a/crates/aiken-lang/src/tests/format.rs +++ b/crates/aiken-lang/src/tests/format.rs @@ -87,6 +87,39 @@ fn format_grouped_expression() { ); } +#[test] +fn format_grouped_expression_2() { + assert_format!( + r#" + fn foo() { + ( y == x ) |> f + } + "# + ); +} + +#[test] +fn format_grouped_expression_3() { + assert_format!( + r#" + fn foo() { + { x |> f } == y + } + "# + ); +} + +#[test] +fn format_grouped_expression_4() { + assert_format!( + r#" + fn foo() { + x |> { f == y } + } + "# + ); +} + #[test] fn format_validator() { assert_format!( diff --git a/crates/aiken-lang/src/tests/snapshots/format_grouped_expression.snap b/crates/aiken-lang/src/tests/snapshots/format_grouped_expression.snap new file mode 100644 index 00000000..b6c5fdc1 --- /dev/null +++ b/crates/aiken-lang/src/tests/snapshots/format_grouped_expression.snap @@ -0,0 +1,8 @@ +--- +source: crates/aiken-lang/src/tests/format.rs +description: "Code:\n\nfn foo() {\n y == { x |> f }\n}\n" +--- +fn foo() { + y == ( x |> f ) +} + diff --git a/crates/aiken-lang/src/tests/snapshots/format_grouped_expression_2.snap b/crates/aiken-lang/src/tests/snapshots/format_grouped_expression_2.snap new file mode 100644 index 00000000..b4485e14 --- /dev/null +++ b/crates/aiken-lang/src/tests/snapshots/format_grouped_expression_2.snap @@ -0,0 +1,8 @@ +--- +source: crates/aiken-lang/src/tests/format.rs +description: "Code:\n\nfn foo() {\n ( y == x ) |> f\n}\n" +--- +fn foo() { + ( y == x ) |> f +} + diff --git a/crates/aiken-lang/src/tests/snapshots/format_grouped_expression_3.snap b/crates/aiken-lang/src/tests/snapshots/format_grouped_expression_3.snap new file mode 100644 index 00000000..31fc7441 --- /dev/null +++ b/crates/aiken-lang/src/tests/snapshots/format_grouped_expression_3.snap @@ -0,0 +1,8 @@ +--- +source: crates/aiken-lang/src/tests/format.rs +description: "Code:\n\nfn foo() {\n { x |> f } == y\n}\n" +--- +fn foo() { + ( x |> f ) == y +} + diff --git a/crates/aiken-lang/src/tests/snapshots/format_grouped_expression_4.snap b/crates/aiken-lang/src/tests/snapshots/format_grouped_expression_4.snap new file mode 100644 index 00000000..0b591aea --- /dev/null +++ b/crates/aiken-lang/src/tests/snapshots/format_grouped_expression_4.snap @@ -0,0 +1,8 @@ +--- +source: crates/aiken-lang/src/tests/format.rs +description: "Code:\n\nfn foo() {\n x |> { f == y }\n}\n" +--- +fn foo() { + x |> f == y +} + From 711825340125dfd3de83c2495fa869458d2ac6dd Mon Sep 17 00:00:00 2001 From: rvcas Date: Sat, 18 Nov 2023 17:15:58 -0500 Subject: [PATCH 07/25] fix: if branches, final_else, and anon fns should all be "top level" --- crates/aiken-lang/src/format.rs | 10 +++++----- examples/gift_card/validators/multi.ak | 4 ++-- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/crates/aiken-lang/src/format.rs b/crates/aiken-lang/src/format.rs index c163d34b..726914c2 100644 --- a/crates/aiken-lang/src/format.rs +++ b/crates/aiken-lang/src/format.rs @@ -610,9 +610,9 @@ impl<'comments> Formatter<'comments> { let args = wrap_args(args.iter().map(|e| (self.fn_arg(e), false))).group(); let body = match body { UntypedExpr::Trace { .. } | UntypedExpr::When { .. } => { - self.expr(body, false).force_break() + self.expr(body, true).force_break() } - _ => self.expr(body, false), + _ => self.expr(body, true), }; let header = "fn".to_doc().append(args); @@ -1071,7 +1071,7 @@ impl<'comments> Formatter<'comments> { let else_begin = line().append("} else {"); - let else_body = line().append(self.expr(final_else, false)).nest(INDENT); + let else_body = line().append(self.expr(final_else, true)).nest(INDENT); let else_end = line().append("}"); @@ -1092,7 +1092,7 @@ impl<'comments> Formatter<'comments> { .append(break_("{", " {")) .group(); - let if_body = line().append(self.expr(&branch.body, false)).nest(INDENT); + let if_body = line().append(self.expr(&branch.body, true)).nest(INDENT); if_begin.append(if_body) } @@ -1146,7 +1146,7 @@ impl<'comments> Formatter<'comments> { let precedence = name.precedence(); let left_precedence = left.binop_precedence(); - let right_precedence = dbg!(right).binop_precedence(); + let right_precedence = right.binop_precedence(); let left = self.expr(left, false); let right = self.expr(right, false); diff --git a/examples/gift_card/validators/multi.ak b/examples/gift_card/validators/multi.ak index d998eb58..c267513f 100644 --- a/examples/gift_card/validators/multi.ak +++ b/examples/gift_card/validators/multi.ak @@ -119,12 +119,12 @@ fn check_mint_and_outputs( when minted_assets is { [] -> True [(minted_asset_name, quantity), ..rest_assets] -> { - expect True = + expect list.any( expected_assets, fn(expected_asset) { expected_asset == minted_asset_name }, ) - expect True = + expect list.any( outputs, fn(output) { From 2ed91780f4569b1602b3853640982900373f5087 Mon Sep 17 00:00:00 2001 From: rvcas Date: Sat, 18 Nov 2023 17:21:43 -0500 Subject: [PATCH 08/25] fix: call arg should be top level --- crates/aiken-lang/src/format.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/aiken-lang/src/format.rs b/crates/aiken-lang/src/format.rs index 726914c2..6ae8fe36 100644 --- a/crates/aiken-lang/src/format.rs +++ b/crates/aiken-lang/src/format.rs @@ -1578,7 +1578,7 @@ impl<'comments> Formatter<'comments> { | UntypedExpr::Sequence { .. } | UntypedExpr::Assignment { .. } => "{" .to_doc() - .append(line().append(self.expr(expr, false)).nest(INDENT)) + .append(line().append(self.expr(expr, true)).nest(INDENT)) .append(line()) .append("}") .force_break(), From abd18656e311637721a4102b4a09cd9788e61b7e Mon Sep 17 00:00:00 2001 From: rvcas Date: Sat, 18 Nov 2023 17:45:41 -0500 Subject: [PATCH 09/25] fix: unable to have newline after expect bool shortcut --- crates/aiken-lang/src/format.rs | 4 ++-- crates/aiken-lang/src/tests/format.rs | 13 +++++++++++++ .../format_preserve_newline_after_bool_expect.snap | 10 ++++++++++ examples/gift_card/validators/multi.ak | 2 ++ 4 files changed, 27 insertions(+), 2 deletions(-) create mode 100644 crates/aiken-lang/src/tests/snapshots/format_preserve_newline_after_bool_expect.snap diff --git a/crates/aiken-lang/src/format.rs b/crates/aiken-lang/src/format.rs index 6ae8fe36..167e8c22 100644 --- a/crates/aiken-lang/src/format.rs +++ b/crates/aiken-lang/src/format.rs @@ -659,8 +659,6 @@ impl<'comments> Formatter<'comments> { kind: AssignmentKind, annotation: &'a Option, ) -> Document<'a> { - self.pop_empty_lines(pattern.location().end); - let keyword = match kind { AssignmentKind::Let => "let", AssignmentKind::Expect => "expect", @@ -673,6 +671,8 @@ impl<'comments> Formatter<'comments> { keyword.to_doc().append(self.case_clause_value(value)) } _ => { + self.pop_empty_lines(pattern.location().end); + let pattern = self.pattern(pattern); let annotation = annotation diff --git a/crates/aiken-lang/src/tests/format.rs b/crates/aiken-lang/src/tests/format.rs index 9c9c12cb..d04dfc30 100644 --- a/crates/aiken-lang/src/tests/format.rs +++ b/crates/aiken-lang/src/tests/format.rs @@ -120,6 +120,19 @@ fn format_grouped_expression_4() { ); } +#[test] +fn format_preserve_newline_after_bool_expect() { + assert_format!( + r#" + fn foo() { + expect 1 == 1 + + False + } + "# + ); +} + #[test] fn format_validator() { assert_format!( diff --git a/crates/aiken-lang/src/tests/snapshots/format_preserve_newline_after_bool_expect.snap b/crates/aiken-lang/src/tests/snapshots/format_preserve_newline_after_bool_expect.snap new file mode 100644 index 00000000..29b82e64 --- /dev/null +++ b/crates/aiken-lang/src/tests/snapshots/format_preserve_newline_after_bool_expect.snap @@ -0,0 +1,10 @@ +--- +source: crates/aiken-lang/src/tests/format.rs +description: "Code:\n\nfn foo() {\n expect 1 == 1\n\n False\n}\n" +--- +fn foo() { + expect 1 == 1 + + False +} + diff --git a/examples/gift_card/validators/multi.ak b/examples/gift_card/validators/multi.ak index c267513f..5d227961 100644 --- a/examples/gift_card/validators/multi.ak +++ b/examples/gift_card/validators/multi.ak @@ -124,6 +124,7 @@ fn check_mint_and_outputs( expected_assets, fn(expected_asset) { expected_asset == minted_asset_name }, ) + expect list.any( outputs, @@ -132,6 +133,7 @@ fn check_mint_and_outputs( datum == InlineDatum(minted_asset_name) && address.payment_credential == validator_cred }, ) + quantity == 1 && check_mint_and_outputs( rest_assets, outputs, From 78b0789cbc1830f7b7464238289e368f0de4d01c Mon Sep 17 00:00:00 2001 From: microproofs Date: Wed, 22 Nov 2023 18:02:21 -0500 Subject: [PATCH 10/25] chore: unit test for pub in validator module warnings closes #681 --- crates/aiken-lang/src/tests/check.rs | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/crates/aiken-lang/src/tests/check.rs b/crates/aiken-lang/src/tests/check.rs index 047aa419..e6815aa7 100644 --- a/crates/aiken-lang/src/tests/check.rs +++ b/crates/aiken-lang/src/tests/check.rs @@ -66,6 +66,25 @@ fn validator_illegal_return_type() { )) } +#[test] +fn validator_useless_pub() { + let source_code = r#" + type Datum { + thing: Int + } + + validator { + pub fn foo(_d: Datum, _r, _c) { + True + } + } + "#; + + let (warnings, _) = check_validator(parse(source_code)).unwrap(); + + assert!(matches!(warnings[0], Warning::PubInValidatorModule { .. })) +} + #[test] fn validator_illegal_arity() { let source_code = r#" From 63f96d13ca3f30aa6e669b589199f3f34aa0d06e Mon Sep 17 00:00:00 2001 From: microproofs Date: Wed, 22 Nov 2023 19:17:45 -0500 Subject: [PATCH 11/25] fix: clippy warning --- crates/aiken-lang/src/tipo/fields.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/aiken-lang/src/tipo/fields.rs b/crates/aiken-lang/src/tipo/fields.rs index e07ccd7b..17afc9cc 100644 --- a/crates/aiken-lang/src/tipo/fields.rs +++ b/crates/aiken-lang/src/tipo/fields.rs @@ -150,9 +150,9 @@ impl FieldMap { self.fields .keys() - .cloned() .filter(|f| !given.contains(f)) .sorted() + .cloned() .collect() } } From 45177cd08b372ebefc34bad5c21f08c4ec5a74e8 Mon Sep 17 00:00:00 2001 From: microproofs Date: Thu, 23 Nov 2023 13:00:24 -0500 Subject: [PATCH 12/25] fix: add missing type checks for the new bls primitives --- crates/aiken-lang/src/gen_uplc.rs | 6 ++++++ crates/aiken-lang/src/gen_uplc/builder.rs | 8 +++++++- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/crates/aiken-lang/src/gen_uplc.rs b/crates/aiken-lang/src/gen_uplc.rs index 787c1a2d..23ee3c2f 100644 --- a/crates/aiken-lang/src/gen_uplc.rs +++ b/crates/aiken-lang/src/gen_uplc.rs @@ -4049,6 +4049,12 @@ impl<'a> CodeGenerator<'a> { Term::equals_string() } else if tipo.is_bytearray() { Term::equals_bytestring() + } else if tipo.is_bls381_12_g1() { + Term::bls12_381_g1_equal() + } else if tipo.is_bls381_12_g2() { + Term::bls12_381_g2_equal() + } else if tipo.is_ml_result() { + panic!("ML Result equality is not supported") } else { Term::equals_data() }; diff --git a/crates/aiken-lang/src/gen_uplc/builder.rs b/crates/aiken-lang/src/gen_uplc/builder.rs index d5732214..782be3dc 100644 --- a/crates/aiken-lang/src/gen_uplc/builder.rs +++ b/crates/aiken-lang/src/gen_uplc/builder.rs @@ -579,6 +579,12 @@ pub fn get_variant_name(t: &Rc) -> String { "_bool".to_string() } else if t.is_bytearray() { "_bytearray".to_string() + } else if t.is_bls381_12_g1() { + "_bls381_12_g1".to_string() + } else if t.is_bls381_12_g2() { + "_bls381_12_g2".to_string() + } else if t.is_ml_result() { + "_ml_result".to_string() } else if t.is_map() { let mut full_type = vec!["_map".to_string()]; let pair_type = &t.get_inner_types()[0]; @@ -1305,7 +1311,7 @@ pub fn convert_constants_to_data(constants: Vec>) -> Vec UplcConstant::Data(PlutusData::BoundedBytes( b.deref().clone().compress().into(), )), - UplcConstant::Bls12_381MlResult(_) => unreachable!("Bls12_381MlResult not supported"), + UplcConstant::Bls12_381MlResult(_) => panic!("Bls12_381MlResult not supported"), }; new_constants.push(constant); } From 689a41ded42178e485bf6cf37044ea72d4114120 Mon Sep 17 00:00:00 2001 From: Pi Lanningham Date: Thu, 9 Nov 2023 14:53:41 -0500 Subject: [PATCH 13/25] Implement a basic watch command This adds the following command ``` aiken watch ``` There are some open questions to answer, though: - I really like the ergonomics of `aiken watch`; but it also makes sense as a flag to `aiken check` or `aiken build` etc.; should we just support the flag, the command, or both? - Right now I duplicated the with_project method, because it forces process::exit(1); Should we refactor this, and if so, how? - Are there other configuration options we want? --- crates/aiken/Cargo.toml | 1 + crates/aiken/src/cmd/check.rs | 5 +++ crates/aiken/src/cmd/mod.rs | 2 + crates/aiken/src/cmd/watch.rs | 71 ++++++++++++++++++++++++++++++ crates/aiken/src/lib.rs | 82 +++++++++++++++++++++++++++++++++++ crates/aiken/src/main.rs | 3 +- 6 files changed, 163 insertions(+), 1 deletion(-) create mode 100644 crates/aiken/src/cmd/watch.rs diff --git a/crates/aiken/Cargo.toml b/crates/aiken/Cargo.toml index 6b87830a..f6b24621 100644 --- a/crates/aiken/Cargo.toml +++ b/crates/aiken/Cargo.toml @@ -25,6 +25,7 @@ hex = "0.4.3" ignore = "0.4.20" indoc = "2.0" miette = { version = "5.5.0", features = ["fancy"] } +notify = "6.1.1" owo-colors = { version = "3.5.0", features = ["supports-colors"] } pallas-addresses = "0.18.0" pallas-codec = "0.18.0" diff --git a/crates/aiken/src/cmd/check.rs b/crates/aiken/src/cmd/check.rs index 5b9aa278..d34f31ed 100644 --- a/crates/aiken/src/cmd/check.rs +++ b/crates/aiken/src/cmd/check.rs @@ -18,6 +18,10 @@ pub struct Args { #[clap(long)] debug: bool, + // When enabled, re-run the command on file changes instead of exiting + #[clap(long)] + watch: bool, + /// Only run tests if they match any of these strings. /// You can match a module with `-m aiken/list` or `-m list`. /// You can match a test with `-m "aiken/list.{map}"` or `-m "aiken/option.{flatten_1}"` @@ -43,6 +47,7 @@ pub fn exec( match_tests, exact_match, no_traces, + watch, }: Args, ) -> miette::Result<()> { crate::with_project(directory, deny, |p| { diff --git a/crates/aiken/src/cmd/mod.rs b/crates/aiken/src/cmd/mod.rs index 83f59ff4..58356166 100644 --- a/crates/aiken/src/cmd/mod.rs +++ b/crates/aiken/src/cmd/mod.rs @@ -4,6 +4,7 @@ use clap::Parser; pub mod blueprint; pub mod build; pub mod check; +pub mod watch; pub mod completion; pub mod docs; pub mod fmt; @@ -23,6 +24,7 @@ pub enum Cmd { Build(build::Args), Address(blueprint::address::Args), Check(check::Args), + Watch(watch::Args), Docs(docs::Args), Add(packages::add::Args), diff --git a/crates/aiken/src/cmd/watch.rs b/crates/aiken/src/cmd/watch.rs new file mode 100644 index 00000000..f00f9e82 --- /dev/null +++ b/crates/aiken/src/cmd/watch.rs @@ -0,0 +1,71 @@ +use notify::{RecursiveMode, Watcher, Event, event::EventAttributes}; +use std::{path::Path, error::Error, time::SystemTime, collections::VecDeque, sync::{Mutex, Arc}}; +#[derive(clap::Args)] +/// Type-check an Aiken project +pub struct Args { + /// Clear the screen between each run + #[clap(long)] + clear: bool, +} + +pub fn exec(Args { clear }: Args) -> miette::Result<()> { + let project = Path::new("../sundae-contracts/aiken").to_path_buf().canonicalize().expect(""); + let build = project.join("build").canonicalize().expect(""); + let lock = project.join("aiken.lock"); + let queue = Arc::new(Mutex::new(VecDeque::new())); + queue.lock().unwrap().push_back(Event { kind: notify::EventKind::Any, paths: vec![], attrs: EventAttributes::new() }); + + let queue_write = queue.clone(); + let mut watcher = notify::recommended_watcher(move |res: notify::Result| { + match res { + Ok(event) => { + match event.kind { + notify::EventKind::Create(_) | + notify::EventKind::Modify(_) | + notify::EventKind::Remove(_) => { + let mut queue = queue_write.lock().expect("lock queue"); + queue.push_back(event.clone()); + drop(queue); + } + _ => {} + } + }, + Err(e) => { + println!("watch error: {:?}", e) + }, + }; + }).expect("watcher"); + let _ = watcher.watch(Path::new("../sundae-contracts/aiken"), RecursiveMode::Recursive); + let queue_read = queue.clone(); + let mut last_evt = SystemTime::UNIX_EPOCH; + loop { + std::thread::sleep(std::time::Duration::from_millis(300)); + + let mut queue = queue_read.lock().expect("lock queue"); + let mut latest = None; + while let Some(evt) = queue.pop_back() { + if evt.paths.iter().any(|p| { + let p = p.canonicalize().expect(""); + p.starts_with(&build) || p.starts_with(&lock) + }) { + continue; + } + latest = Some(evt); + } + drop(queue); + if let Some(evt) = latest { + if clear { + println!("{esc}c", esc = 27 as char); + } + let _ = crate::with_project_ok(Some(project.clone()), false, |p| { + p.check( + false, + None, + true, + false, + false.into(), + ) + }); + } + } +} diff --git a/crates/aiken/src/lib.rs b/crates/aiken/src/lib.rs index 474a48d3..7aad48ad 100644 --- a/crates/aiken/src/lib.rs +++ b/crates/aiken/src/lib.rs @@ -94,6 +94,88 @@ where Ok(()) } +// TODO: we probably want to rework with_project slightly to avoid duplication here, +// but this is a quick hack to get the aiken watch working +pub fn with_project_ok(directory: Option, deny: bool, mut action: A) -> miette::Result<()> +where + A: FnMut(&mut Project) -> Result<(), Vec>, +{ + let project_path = if let Some(d) = directory { + d + } else { + env::current_dir().into_diagnostic()? + }; + + let mut project = match Project::new(project_path, Terminal) { + Ok(p) => p, + Err(e) => { + e.report(); + return Ok(()); + } + }; + + let build_result = action(&mut project); + + let warnings = project.warnings(); + + let warning_count = warnings.len(); + + for warning in &warnings { + warning.report() + } + + let plural = if warning_count == 1 { "" } else { "s" }; + + if let Err(errs) = build_result { + for err in &errs { + err.report() + } + + eprintln!( + "\n{}", + "Summary" + .if_supports_color(Stderr, |s| s.purple()) + .if_supports_color(Stderr, |s| s.bold()) + ); + + let warning_text = format!("{warning_count} warning{plural}"); + + let plural = if errs.len() == 1 { "" } else { "s" }; + + let error_text = format!("{} error{}", errs.len(), plural); + + let full_summary = format!( + " {}, {}", + error_text.if_supports_color(Stderr, |s| s.red()), + warning_text.if_supports_color(Stderr, |s| s.yellow()) + ); + + eprintln!("{full_summary}"); + + return Ok(()); + } else { + eprintln!( + "\n{}", + "Summary" + .if_supports_color(Stderr, |s| s.purple()) + .if_supports_color(Stderr, |s| s.bold()) + ); + + let warning_text = format!("{warning_count} warning{plural}"); + + eprintln!( + " 0 errors, {}", + warning_text.if_supports_color(Stderr, |s| s.yellow()), + ); + } + + if warning_count > 0 && deny { + return Ok(()); + } + + Ok(()) +} + #[derive(Debug, Default, Clone, Copy)] pub struct Terminal; diff --git a/crates/aiken/src/main.rs b/crates/aiken/src/main.rs index 777766c8..62e9828e 100644 --- a/crates/aiken/src/main.rs +++ b/crates/aiken/src/main.rs @@ -1,6 +1,6 @@ use aiken::cmd::{ blueprint::{self, address}, - build, check, completion, docs, fmt, lsp, new, + build, check, watch, completion, docs, fmt, lsp, new, packages::{self, add}, tx, uplc, Cmd, }; @@ -17,6 +17,7 @@ fn main() -> miette::Result<()> { Cmd::Build(args) => build::exec(args), Cmd::Address(args) => address::exec(args), Cmd::Check(args) => check::exec(args), + Cmd::Watch(args) => watch::exec(args), Cmd::Docs(args) => docs::exec(args), Cmd::Add(args) => add::exec(args), Cmd::Blueprint(args) => blueprint::exec(args), From 771f6d160120c932700977529ef0eb4d155bf005 Mon Sep 17 00:00:00 2001 From: Pi Lanningham Date: Thu, 9 Nov 2023 14:59:07 -0500 Subject: [PATCH 14/25] Formatting and check --- crates/aiken/src/cmd/check.rs | 2 +- crates/aiken/src/cmd/mod.rs | 2 +- crates/aiken/src/cmd/watch.rs | 61 +++++++++++++++++++---------------- crates/aiken/src/lib.rs | 6 +++- crates/aiken/src/main.rs | 4 +-- 5 files changed, 43 insertions(+), 32 deletions(-) diff --git a/crates/aiken/src/cmd/check.rs b/crates/aiken/src/cmd/check.rs index d34f31ed..0372b026 100644 --- a/crates/aiken/src/cmd/check.rs +++ b/crates/aiken/src/cmd/check.rs @@ -47,7 +47,7 @@ pub fn exec( match_tests, exact_match, no_traces, - watch, + .. }: Args, ) -> miette::Result<()> { crate::with_project(directory, deny, |p| { diff --git a/crates/aiken/src/cmd/mod.rs b/crates/aiken/src/cmd/mod.rs index 58356166..b96c0727 100644 --- a/crates/aiken/src/cmd/mod.rs +++ b/crates/aiken/src/cmd/mod.rs @@ -4,7 +4,6 @@ use clap::Parser; pub mod blueprint; pub mod build; pub mod check; -pub mod watch; pub mod completion; pub mod docs; pub mod fmt; @@ -13,6 +12,7 @@ pub mod new; pub mod packages; pub mod tx; pub mod uplc; +pub mod watch; /// Aiken: a smart-contract language and toolchain for Cardano #[derive(Parser)] diff --git a/crates/aiken/src/cmd/watch.rs b/crates/aiken/src/cmd/watch.rs index f00f9e82..efc224d7 100644 --- a/crates/aiken/src/cmd/watch.rs +++ b/crates/aiken/src/cmd/watch.rs @@ -1,5 +1,9 @@ -use notify::{RecursiveMode, Watcher, Event, event::EventAttributes}; -use std::{path::Path, error::Error, time::SystemTime, collections::VecDeque, sync::{Mutex, Arc}}; +use notify::{event::EventAttributes, Event, RecursiveMode, Watcher}; +use std::{ + collections::VecDeque, + path::Path, + sync::{Arc, Mutex}, +}; #[derive(clap::Args)] /// Type-check an Aiken project pub struct Args { @@ -9,62 +13,65 @@ pub struct Args { } pub fn exec(Args { clear }: Args) -> miette::Result<()> { - let project = Path::new("../sundae-contracts/aiken").to_path_buf().canonicalize().expect(""); + let project = Path::new("../sundae-contracts/aiken") + .to_path_buf() + .canonicalize() + .expect(""); let build = project.join("build").canonicalize().expect(""); let lock = project.join("aiken.lock"); let queue = Arc::new(Mutex::new(VecDeque::new())); - queue.lock().unwrap().push_back(Event { kind: notify::EventKind::Any, paths: vec![], attrs: EventAttributes::new() }); + queue.lock().unwrap().push_back(Event { + kind: notify::EventKind::Any, + paths: vec![], + attrs: EventAttributes::new(), + }); let queue_write = queue.clone(); let mut watcher = notify::recommended_watcher(move |res: notify::Result| { match res { - Ok(event) => { - match event.kind { - notify::EventKind::Create(_) | - notify::EventKind::Modify(_) | - notify::EventKind::Remove(_) => { - let mut queue = queue_write.lock().expect("lock queue"); - queue.push_back(event.clone()); - drop(queue); - } - _ => {} + Ok(event) => match event.kind { + notify::EventKind::Create(_) + | notify::EventKind::Modify(_) + | notify::EventKind::Remove(_) => { + let mut queue = queue_write.lock().expect("lock queue"); + queue.push_back(event.clone()); + drop(queue); } + _ => {} }, Err(e) => { println!("watch error: {:?}", e) - }, + } }; - }).expect("watcher"); - let _ = watcher.watch(Path::new("../sundae-contracts/aiken"), RecursiveMode::Recursive); + }) + .expect("watcher"); + let _ = watcher.watch( + Path::new("../sundae-contracts/aiken"), + RecursiveMode::Recursive, + ); let queue_read = queue.clone(); - let mut last_evt = SystemTime::UNIX_EPOCH; loop { std::thread::sleep(std::time::Duration::from_millis(300)); let mut queue = queue_read.lock().expect("lock queue"); let mut latest = None; + // debounce the events, and ignore build/lock changes, because they come in in large batches while let Some(evt) = queue.pop_back() { if evt.paths.iter().any(|p| { let p = p.canonicalize().expect(""); p.starts_with(&build) || p.starts_with(&lock) - }) { + }) { continue; } latest = Some(evt); } drop(queue); - if let Some(evt) = latest { + if latest.is_some() { if clear { println!("{esc}c", esc = 27 as char); } let _ = crate::with_project_ok(Some(project.clone()), false, |p| { - p.check( - false, - None, - true, - false, - false.into(), - ) + p.check(false, None, true, false, false.into()) }); } } diff --git a/crates/aiken/src/lib.rs b/crates/aiken/src/lib.rs index 7aad48ad..d730ee9f 100644 --- a/crates/aiken/src/lib.rs +++ b/crates/aiken/src/lib.rs @@ -96,7 +96,11 @@ where // TODO: we probably want to rework with_project slightly to avoid duplication here, // but this is a quick hack to get the aiken watch working -pub fn with_project_ok(directory: Option, deny: bool, mut action: A) -> miette::Result<()> +pub fn with_project_ok( + directory: Option, + deny: bool, + mut action: A, +) -> miette::Result<()> where A: FnMut(&mut Project) -> Result<(), Vec>, { diff --git a/crates/aiken/src/main.rs b/crates/aiken/src/main.rs index 62e9828e..16791d25 100644 --- a/crates/aiken/src/main.rs +++ b/crates/aiken/src/main.rs @@ -1,8 +1,8 @@ use aiken::cmd::{ blueprint::{self, address}, - build, check, watch, completion, docs, fmt, lsp, new, + build, check, completion, docs, fmt, lsp, new, packages::{self, add}, - tx, uplc, Cmd, + tx, uplc, watch, Cmd, }; use aiken_project::{config, pretty}; From 5068da3a17bca72a87bbbda4ae7c682b34798230 Mon Sep 17 00:00:00 2001 From: Pi Lanningham Date: Fri, 10 Nov 2023 21:35:55 -0500 Subject: [PATCH 15/25] Refactor into cargo-project Rather than have this logic in the aiken binary, this provides a generic mechanism to do "something" on file change events. KtorZ is going to handle wiring it up to the CLI in the best way for the project. I tried to write some tests for this, but it's hard to isolate the watcher logic without wrestling with the borrow checker, or overly neutering this utility. --- crates/aiken-project/Cargo.toml | 1 + crates/aiken-project/src/lib.rs | 1 + crates/aiken-project/src/watch.rs | 119 ++++++++++++++++++++++++++++++ crates/aiken/Cargo.toml | 1 - crates/aiken/src/cmd/mod.rs | 2 - crates/aiken/src/cmd/watch.rs | 78 -------------------- crates/aiken/src/main.rs | 3 +- 7 files changed, 122 insertions(+), 83 deletions(-) create mode 100644 crates/aiken-project/src/watch.rs delete mode 100644 crates/aiken/src/cmd/watch.rs diff --git a/crates/aiken-project/Cargo.toml b/crates/aiken-project/Cargo.toml index 1e425cf3..fa7e8bc5 100644 --- a/crates/aiken-project/Cargo.toml +++ b/crates/aiken-project/Cargo.toml @@ -26,6 +26,7 @@ indexmap = "1.9.2" itertools = "0.10.5" miette = { version = "5.9.0", features = ["fancy"] } minicbor = "0.19.1" +notify = "6.1.1" owo-colors = { version = "3.5.0", features = ["supports-colors"] } pallas = "0.18.0" pallas-traverse = "0.18.0" diff --git a/crates/aiken-project/src/lib.rs b/crates/aiken-project/src/lib.rs index 9b0f14c2..b3f0d3ba 100644 --- a/crates/aiken-project/src/lib.rs +++ b/crates/aiken-project/src/lib.rs @@ -14,6 +14,7 @@ pub mod script; pub mod telemetry; #[cfg(test)] mod tests; +pub mod watch; use crate::blueprint::{ definitions::Definitions, diff --git a/crates/aiken-project/src/watch.rs b/crates/aiken-project/src/watch.rs new file mode 100644 index 00000000..ac6fea08 --- /dev/null +++ b/crates/aiken-project/src/watch.rs @@ -0,0 +1,119 @@ +use miette::IntoDiagnostic; +use notify::{Event, RecursiveMode, Watcher}; +use std::{ + collections::VecDeque, + env, + path::PathBuf, + sync::{Arc, Mutex}, +}; + +use crate::{telemetry, Project}; + +/// A default filter for file events that catches the most relevant "source" changes +pub fn default_filter(evt: &Event) -> bool { + // Only watch for changes to .ak and aiken.toml files, and ignore the build directory + let source_file = evt + .paths + .iter() + .any(|p| p.ends_with(".ak") || p.ends_with("aiken.toml")); + let build_dir = evt + .paths + .iter() + .all(|p| p.ancestors().any(|a| a.ends_with("build"))); + match evt.kind { + notify::EventKind::Create(_) + | notify::EventKind::Modify(_) + | notify::EventKind::Remove(_) => source_file && !build_dir, + _ => false, + } +} + +/// Run a function each time a file in the project changes +/// +/// ``` +/// use aiken_project::watch::{watch_project, default_filter}; +/// use aiken_project::Project; +/// watch_project(None, Terminal, default_filter, 500, |project| { +/// println!("Project changed!"); +/// Ok(()) +/// }) +/// ``` +pub fn watch_project( + directory: Option, + events: T, + filter: F, + debounce: u32, + mut action: A, +) -> miette::Result<()> +where + T: Copy + telemetry::EventListener, + F: Fn(&Event) -> bool, + A: FnMut(&mut Project) -> Result<(), Vec>, +{ + let project_path = directory.unwrap_or(env::current_dir().into_diagnostic()?); + + // Set up a queue for events, primarily so we can debounce on related events + let queue = Arc::new(Mutex::new(VecDeque::new())); + + // pre-seed that queue with a single event, so it builds once at the start + queue.lock().unwrap().push_back(Event::default()); + + // Spawn a file-watcher that will put each change event on the queue + let queue_write = queue.clone(); + let mut watcher = notify::recommended_watcher(move |res: notify::Result| { + match res { + Ok(event) => queue_write + .lock() + .expect("lock queue") + .push_back(event.clone()), + Err(e) => { + // TODO: miette diagnostic? + println!( + "Encountered an error while monitoring for file changes: {:?}", + e + ) + } + }; + }) + .into_diagnostic()?; + + // Start watching for any changes in the project directory + let _ = watcher.watch(project_path.as_path(), RecursiveMode::Recursive); + + // And then start reading from the queue + let queue_read = queue.clone(); + loop { + // We sleep for the debounce interval, because notify will dump 12 related events into the queue all at once + std::thread::sleep(std::time::Duration::from_millis(debounce.into())); + + // Grab the lock, and pop all events except the last one off the queue + let mut queue = queue_read.lock().expect("lock queue"); + let mut latest = None; + // debounce the events, and ignore build/lock changes, because they come in in large batches + while let Some(evt) = queue.pop_back() { + // check if this event is meaningful to the caller + if !filter(&evt) { + continue; + } + latest = Some(evt); + } + // release the lock here, in case other events come in + drop(queue); + + // If we have an event that survived the filter, then we can construct the project and invoke the action + if latest.is_some() { + let mut project = match Project::new(project_path.clone(), events) { + Ok(p) => p, + Err(e) => { + // TODO: what should we actually do here? + e.report(); + return Err(miette::Report::msg("??")); + } + }; + + // Invoke the action, and abort on an error + // TODO: what should we actually do with the error here? + action(&mut project).or(Err(miette::Report::msg("??")))?; + } + } +} diff --git a/crates/aiken/Cargo.toml b/crates/aiken/Cargo.toml index f6b24621..6b87830a 100644 --- a/crates/aiken/Cargo.toml +++ b/crates/aiken/Cargo.toml @@ -25,7 +25,6 @@ hex = "0.4.3" ignore = "0.4.20" indoc = "2.0" miette = { version = "5.5.0", features = ["fancy"] } -notify = "6.1.1" owo-colors = { version = "3.5.0", features = ["supports-colors"] } pallas-addresses = "0.18.0" pallas-codec = "0.18.0" diff --git a/crates/aiken/src/cmd/mod.rs b/crates/aiken/src/cmd/mod.rs index b96c0727..83f59ff4 100644 --- a/crates/aiken/src/cmd/mod.rs +++ b/crates/aiken/src/cmd/mod.rs @@ -12,7 +12,6 @@ pub mod new; pub mod packages; pub mod tx; pub mod uplc; -pub mod watch; /// Aiken: a smart-contract language and toolchain for Cardano #[derive(Parser)] @@ -24,7 +23,6 @@ pub enum Cmd { Build(build::Args), Address(blueprint::address::Args), Check(check::Args), - Watch(watch::Args), Docs(docs::Args), Add(packages::add::Args), diff --git a/crates/aiken/src/cmd/watch.rs b/crates/aiken/src/cmd/watch.rs deleted file mode 100644 index efc224d7..00000000 --- a/crates/aiken/src/cmd/watch.rs +++ /dev/null @@ -1,78 +0,0 @@ -use notify::{event::EventAttributes, Event, RecursiveMode, Watcher}; -use std::{ - collections::VecDeque, - path::Path, - sync::{Arc, Mutex}, -}; -#[derive(clap::Args)] -/// Type-check an Aiken project -pub struct Args { - /// Clear the screen between each run - #[clap(long)] - clear: bool, -} - -pub fn exec(Args { clear }: Args) -> miette::Result<()> { - let project = Path::new("../sundae-contracts/aiken") - .to_path_buf() - .canonicalize() - .expect(""); - let build = project.join("build").canonicalize().expect(""); - let lock = project.join("aiken.lock"); - let queue = Arc::new(Mutex::new(VecDeque::new())); - queue.lock().unwrap().push_back(Event { - kind: notify::EventKind::Any, - paths: vec![], - attrs: EventAttributes::new(), - }); - - let queue_write = queue.clone(); - let mut watcher = notify::recommended_watcher(move |res: notify::Result| { - match res { - Ok(event) => match event.kind { - notify::EventKind::Create(_) - | notify::EventKind::Modify(_) - | notify::EventKind::Remove(_) => { - let mut queue = queue_write.lock().expect("lock queue"); - queue.push_back(event.clone()); - drop(queue); - } - _ => {} - }, - Err(e) => { - println!("watch error: {:?}", e) - } - }; - }) - .expect("watcher"); - let _ = watcher.watch( - Path::new("../sundae-contracts/aiken"), - RecursiveMode::Recursive, - ); - let queue_read = queue.clone(); - loop { - std::thread::sleep(std::time::Duration::from_millis(300)); - - let mut queue = queue_read.lock().expect("lock queue"); - let mut latest = None; - // debounce the events, and ignore build/lock changes, because they come in in large batches - while let Some(evt) = queue.pop_back() { - if evt.paths.iter().any(|p| { - let p = p.canonicalize().expect(""); - p.starts_with(&build) || p.starts_with(&lock) - }) { - continue; - } - latest = Some(evt); - } - drop(queue); - if latest.is_some() { - if clear { - println!("{esc}c", esc = 27 as char); - } - let _ = crate::with_project_ok(Some(project.clone()), false, |p| { - p.check(false, None, true, false, false.into()) - }); - } - } -} diff --git a/crates/aiken/src/main.rs b/crates/aiken/src/main.rs index 16791d25..777766c8 100644 --- a/crates/aiken/src/main.rs +++ b/crates/aiken/src/main.rs @@ -2,7 +2,7 @@ use aiken::cmd::{ blueprint::{self, address}, build, check, completion, docs, fmt, lsp, new, packages::{self, add}, - tx, uplc, watch, Cmd, + tx, uplc, Cmd, }; use aiken_project::{config, pretty}; @@ -17,7 +17,6 @@ fn main() -> miette::Result<()> { Cmd::Build(args) => build::exec(args), Cmd::Address(args) => address::exec(args), Cmd::Check(args) => check::exec(args), - Cmd::Watch(args) => watch::exec(args), Cmd::Docs(args) => docs::exec(args), Cmd::Add(args) => add::exec(args), Cmd::Blueprint(args) => blueprint::exec(args), From 5945a9515b2c22fafdb5c65be2b9e5772085d472 Mon Sep 17 00:00:00 2001 From: Pi Lanningham Date: Fri, 10 Nov 2023 21:52:26 -0500 Subject: [PATCH 16/25] Disable the doctest, since I don't have an impl of EventListener I can use --- crates/aiken-project/src/watch.rs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/crates/aiken-project/src/watch.rs b/crates/aiken-project/src/watch.rs index ac6fea08..fc95c426 100644 --- a/crates/aiken-project/src/watch.rs +++ b/crates/aiken-project/src/watch.rs @@ -30,13 +30,14 @@ pub fn default_filter(evt: &Event) -> bool { /// Run a function each time a file in the project changes /// -/// ``` +/// ```text +/// // Note: doctest disabled, because aiken_project doesn't have an implementation of EventListener I can use /// use aiken_project::watch::{watch_project, default_filter}; -/// use aiken_project::Project; +/// use aiken_project::{Project}; /// watch_project(None, Terminal, default_filter, 500, |project| { /// println!("Project changed!"); /// Ok(()) -/// }) +/// }); /// ``` pub fn watch_project( directory: Option, From 4bb424ba7801ae4b833bfc88e60752a850eed01e Mon Sep 17 00:00:00 2001 From: Pi Lanningham Date: Fri, 10 Nov 2023 23:20:12 -0500 Subject: [PATCH 17/25] Fix a small bug with the filtering --- crates/aiken-project/src/watch.rs | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/crates/aiken-project/src/watch.rs b/crates/aiken-project/src/watch.rs index fc95c426..61dda460 100644 --- a/crates/aiken-project/src/watch.rs +++ b/crates/aiken-project/src/watch.rs @@ -3,6 +3,7 @@ use notify::{Event, RecursiveMode, Watcher}; use std::{ collections::VecDeque, env, + ffi::OsStr, path::PathBuf, sync::{Arc, Mutex}, }; @@ -15,12 +16,13 @@ pub fn default_filter(evt: &Event) -> bool { let source_file = evt .paths .iter() - .any(|p| p.ends_with(".ak") || p.ends_with("aiken.toml")); + .any(|p| p.extension() == Some(OsStr::new("ak")) || p.ends_with("aiken.toml")); let build_dir = evt .paths .iter() .all(|p| p.ancestors().any(|a| a.ends_with("build"))); match evt.kind { + notify::EventKind::Any => true, notify::EventKind::Create(_) | notify::EventKind::Modify(_) | notify::EventKind::Remove(_) => source_file && !build_dir, @@ -56,8 +58,11 @@ where // Set up a queue for events, primarily so we can debounce on related events let queue = Arc::new(Mutex::new(VecDeque::new())); - // pre-seed that queue with a single event, so it builds once at the start - queue.lock().unwrap().push_back(Event::default()); + // Run the action once, to start + queue + .lock() + .expect("lock queue") + .push_back(Event::default()); // Spawn a file-watcher that will put each change event on the queue let queue_write = queue.clone(); From d04094560bca17a86a5dfefdf0c4118562dc481a Mon Sep 17 00:00:00 2001 From: Pi Lanningham Date: Fri, 10 Nov 2023 23:21:09 -0500 Subject: [PATCH 18/25] Add an example usage in the check command Feel free to do this differently, I just implemented it because i'm actually using it heh --- crates/aiken/src/cmd/check.rs | 98 +++++++++++++++++++++++++++++++---- 1 file changed, 88 insertions(+), 10 deletions(-) diff --git a/crates/aiken/src/cmd/check.rs b/crates/aiken/src/cmd/check.rs index 0372b026..59e7c5fb 100644 --- a/crates/aiken/src/cmd/check.rs +++ b/crates/aiken/src/cmd/check.rs @@ -1,5 +1,10 @@ use std::path::PathBuf; +use aiken_project::watch; +use owo_colors::{OwoColorize, Stream::Stderr}; + +use crate::Terminal; + #[derive(clap::Args)] /// Type-check an Aiken project pub struct Args { @@ -18,10 +23,14 @@ pub struct Args { #[clap(long)] debug: bool, - // When enabled, re-run the command on file changes instead of exiting + /// When enabled, re-run the command on file changes instead of exiting #[clap(long)] watch: bool, + /// When enabled, clear the screen before running + #[clap(long)] + clear: bool, + /// Only run tests if they match any of these strings. /// You can match a module with `-m aiken/list` or `-m list`. /// You can match a test with `-m "aiken/list.{map}"` or `-m "aiken/option.{flatten_1}"` @@ -47,16 +56,85 @@ pub fn exec( match_tests, exact_match, no_traces, + watch, + clear, .. }: Args, ) -> miette::Result<()> { - crate::with_project(directory, deny, |p| { - p.check( - skip_tests, - match_tests.clone(), - debug, - exact_match, - (!no_traces).into(), - ) - }) + if watch { + watch::watch_project(directory, Terminal, watch::default_filter, 500, |p| { + if clear { + println!("{esc}c", esc = 27 as char); + } + let build_result = p.check( + skip_tests, + match_tests.clone(), + debug, + exact_match, + (!no_traces).into(), + ); + + let warnings = p.warnings(); + + let warning_count = warnings.len(); + + for warning in &warnings { + warning.report() + } + + let plural = if warning_count == 1 { "" } else { "s" }; + + if let Err(errs) = build_result { + for err in &errs { + err.report() + } + + eprintln!( + "\n{}", + "Summary" + .if_supports_color(Stderr, |s| s.purple()) + .if_supports_color(Stderr, |s| s.bold()) + ); + + let warning_text = format!("{warning_count} warning{plural}"); + + let plural = if errs.len() == 1 { "" } else { "s" }; + + let error_text = format!("{} error{}", errs.len(), plural); + + let full_summary = format!( + " {}, {}", + error_text.if_supports_color(Stderr, |s| s.red()), + warning_text.if_supports_color(Stderr, |s| s.yellow()) + ); + + eprintln!("{full_summary}"); + } else { + eprintln!( + "\n{}", + "Summary" + .if_supports_color(Stderr, |s| s.purple()) + .if_supports_color(Stderr, |s| s.bold()) + ); + + let warning_text = format!("{warning_count} warning{plural}"); + + eprintln!( + " 0 errors, {}", + warning_text.if_supports_color(Stderr, |s| s.yellow()), + ); + } + Ok(()) + }) + } else { + crate::with_project(directory, deny, |p| { + p.check( + skip_tests, + match_tests.clone(), + debug, + exact_match, + (!no_traces).into(), + ) + }) + } } From 1ca81ec13308dfd97dbeece1f90333f9e37e65ab Mon Sep 17 00:00:00 2001 From: KtorZ Date: Fri, 24 Nov 2023 09:44:02 +0100 Subject: [PATCH 19/25] Turn evaluation hints into strings earlier, to make project's Error thread-safe. --- crates/aiken-project/src/error.rs | 40 ++++-------------------- crates/aiken-project/src/lib.rs | 6 +++- crates/aiken-project/src/script.rs | 49 ++++++++++++++++++++++++++++-- 3 files changed, 58 insertions(+), 37 deletions(-) diff --git a/crates/aiken-project/src/error.rs b/crates/aiken-project/src/error.rs index 0dcd2653..cc0620bb 100644 --- a/crates/aiken-project/src/error.rs +++ b/crates/aiken-project/src/error.rs @@ -1,9 +1,6 @@ -use crate::{ - blueprint::error as blueprint, deps::manifest::Package, package_name::PackageName, pretty, - script::EvalHint, -}; +use crate::{blueprint::error as blueprint, deps::manifest::Package, package_name::PackageName}; use aiken_lang::{ - ast::{self, BinOp, Span}, + ast::{self, Span}, error::ExtraData, parser::error::ParseError, tipo, @@ -18,7 +15,6 @@ use std::{ ops::Deref, path::{Path, PathBuf}, }; -use uplc::machine::cost_model::ExBudget; use zip::result::ZipError; #[allow(dead_code)] @@ -97,7 +93,7 @@ pub enum Error { path: PathBuf, verbose: bool, src: String, - evaluation_hint: Option, + evaluation_hint: Option, }, #[error( @@ -313,33 +309,9 @@ impl Diagnostic for Error { Error::MissingManifest { .. } => Some(Box::new("Try running `aiken new ` to initialise a project with an example manifest.")), Error::TomlLoading { .. } => None, Error::Format { .. } => None, - 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.clone().eval(budget).result() { - Ok(term) => format!("{term}"), - Err(err) => format!("{err}"), - }); - let right = pretty::boxed("right", &match hint.right.clone().eval(budget).result() { - 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)) - } - } + Error::TestFailure { evaluation_hint, .. } => match evaluation_hint { + None => None, + Some(hint) => Some(Box::new(hint.to_string())) }, Error::Http(_) => None, Error::ZipExtract(_) => None, diff --git a/crates/aiken-project/src/lib.rs b/crates/aiken-project/src/lib.rs index b3f0d3ba..72f6a052 100644 --- a/crates/aiken-project/src/lib.rs +++ b/crates/aiken-project/src/lib.rs @@ -329,7 +329,11 @@ where Some(Error::TestFailure { name: e.script.name.clone(), path: e.script.input_path.clone(), - evaluation_hint: e.script.evaluation_hint.clone(), + evaluation_hint: e + .script + .evaluation_hint + .as_ref() + .map(|hint| hint.to_string()), src: e.script.program.to_pretty(), verbose, }) diff --git a/crates/aiken-project/src/script.rs b/crates/aiken-project/src/script.rs index 58b83882..ce3fc636 100644 --- a/crates/aiken-project/src/script.rs +++ b/crates/aiken-project/src/script.rs @@ -1,6 +1,9 @@ -use crate::{ExBudget, Term}; +use crate::{pretty, ExBudget, Term}; use aiken_lang::ast::BinOp; -use std::path::PathBuf; +use std::{ + fmt::{self, Display}, + path::PathBuf, +}; use uplc::ast::{NamedDeBruijn, Program}; #[derive(Debug, Clone)] @@ -42,6 +45,48 @@ pub struct EvalHint { pub right: Program, } +impl Display for EvalHint { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + let unlimited_budget = ExBudget { + mem: i64::MAX, + cpu: i64::MAX, + }; + + let left = pretty::boxed( + "left", + &match self.left.clone().eval(unlimited_budget).result() { + Ok(term) => format!("{term}"), + Err(err) => format!("{err}"), + }, + ); + let right = pretty::boxed( + "right", + &match self.right.clone().eval(unlimited_budget).result() { + Ok(term) => format!("{term}"), + Err(err) => format!("{err}"), + }, + ); + let msg = match self.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, + } + .ok_or(fmt::Error::default())?; + + f.write_str(&msg) + } +} + #[derive(Debug)] pub struct EvalInfo { pub success: bool, From 4adedaac15888caa714fcd705af944d9c4fb57e2 Mon Sep 17 00:00:00 2001 From: KtorZ Date: Sat, 25 Nov 2023 13:09:26 +0100 Subject: [PATCH 20/25] Remove unnecessary 'Rc' in function signature. --- crates/aiken-lang/src/tipo/error.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/crates/aiken-lang/src/tipo/error.rs b/crates/aiken-lang/src/tipo/error.rs index b2fa7b56..493d0e7f 100644 --- a/crates/aiken-lang/src/tipo/error.rs +++ b/crates/aiken-lang/src/tipo/error.rs @@ -1131,15 +1131,15 @@ fn suggest_constructor_pattern( } fn suggest_unify( - expected: &Rc, - given: &Rc, + expected: &Type, + given: &Type, situation: &Option, rigid_type_names: &HashMap, ) -> String { let expected_str = expected.to_pretty_with_names(rigid_type_names.clone(), 0); let given_str = given.to_pretty_with_names(rigid_type_names.clone(), 0); - let (expected, given) = match (expected.as_ref(), given.as_ref()) { + let (expected, given) = match (expected, given) { ( Type::App { module: expected_module, From 777d30b8ac8217179de649a895df86191d6e27d8 Mon Sep 17 00:00:00 2001 From: KtorZ Date: Sat, 25 Nov 2023 13:26:24 +0100 Subject: [PATCH 21/25] Rework 'with_project' to avoid early process exit. --- crates/aiken/src/lib.rs | 175 +++++++++++++-------------------------- crates/aiken/src/main.rs | 11 ++- 2 files changed, 66 insertions(+), 120 deletions(-) diff --git a/crates/aiken/src/lib.rs b/crates/aiken/src/lib.rs index d730ee9f..a2627f4c 100644 --- a/crates/aiken/src/lib.rs +++ b/crates/aiken/src/lib.rs @@ -4,16 +4,34 @@ use aiken_project::{ telemetry::{self, DownloadSource}, Project, }; +use miette::Diagnostic; use miette::IntoDiagnostic; use owo_colors::{ OwoColorize, Stream::{self, Stderr}, }; -use std::{collections::BTreeMap, env, path::PathBuf, process}; +use std::{ + collections::BTreeMap, + env, + fmt::{self, Display}, + path::PathBuf, +}; use uplc::machine::cost_model::ExBudget; pub mod cmd; +#[derive(Debug, Diagnostic, thiserror::Error)] +enum ExitFailure { + #[error("")] + ExitFailure, +} + +impl ExitFailure { + fn into_report() -> miette::Report { + ExitFailure::ExitFailure.into() + } +} + pub fn with_project(directory: Option, deny: bool, mut action: A) -> miette::Result<()> where A: FnMut(&mut Project) -> Result<(), Vec>, @@ -25,12 +43,12 @@ where }; let mut project = match Project::new(project_path, Terminal) { - Ok(p) => p, + Ok(p) => Ok(p), Err(e) => { e.report(); - process::exit(1); + Err(ExitFailure::into_report()) } - }; + }?; let build_result = action(&mut project); @@ -42,8 +60,6 @@ where warning.report() } - let plural = if warning_count == 1 { "" } else { "s" }; - if let Err(errs) = build_result { for err in &errs { err.report() @@ -51,133 +67,58 @@ where eprintln!( "\n{}", - "Summary" - .if_supports_color(Stderr, |s| s.purple()) - .if_supports_color(Stderr, |s| s.bold()) + Summary { + warning_count, + error_count: errs.len(), + } ); - let warning_text = format!("{warning_count} warning{plural}"); - - let plural = if errs.len() == 1 { "" } else { "s" }; - - let error_text = format!("{} error{}", errs.len(), plural); - - let full_summary = format!( - " {}, {}", - error_text.if_supports_color(Stderr, |s| s.red()), - warning_text.if_supports_color(Stderr, |s| s.yellow()) - ); - - eprintln!("{full_summary}"); - - process::exit(1); + return Err(ExitFailure::into_report()); } else { eprintln!( "\n{}", - "Summary" - .if_supports_color(Stderr, |s| s.purple()) - .if_supports_color(Stderr, |s| s.bold()) - ); - - let warning_text = format!("{warning_count} warning{plural}"); - - eprintln!( - " 0 errors, {}", - warning_text.if_supports_color(Stderr, |s| s.yellow()), + Summary { + error_count: 0, + warning_count + } ); } if warning_count > 0 && deny { - process::exit(1); + Err(ExitFailure::into_report()) + } else { + Ok(()) } - - Ok(()) } -// TODO: we probably want to rework with_project slightly to avoid duplication here, -// but this is a quick hack to get the aiken watch working -pub fn with_project_ok( - directory: Option, - deny: bool, - mut action: A, -) -> miette::Result<()> -where - A: FnMut(&mut Project) -> Result<(), Vec>, -{ - let project_path = if let Some(d) = directory { - d - } else { - env::current_dir().into_diagnostic()? - }; +struct Summary { + warning_count: usize, + error_count: usize, +} - let mut project = match Project::new(project_path, Terminal) { - Ok(p) => p, - Err(e) => { - e.report(); - return Ok(()); - } - }; - - let build_result = action(&mut project); - - let warnings = project.warnings(); - - let warning_count = warnings.len(); - - for warning in &warnings { - warning.report() - } - - let plural = if warning_count == 1 { "" } else { "s" }; - - if let Err(errs) = build_result { - for err in &errs { - err.report() - } - - eprintln!( - "\n{}", +impl Display for Summary { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + f.write_str(&format!( + "{}\n {} {}, {} {}", "Summary" .if_supports_color(Stderr, |s| s.purple()) - .if_supports_color(Stderr, |s| s.bold()) - ); - - let warning_text = format!("{warning_count} warning{plural}"); - - let plural = if errs.len() == 1 { "" } else { "s" }; - - let error_text = format!("{} error{}", errs.len(), plural); - - let full_summary = format!( - " {}, {}", - error_text.if_supports_color(Stderr, |s| s.red()), - warning_text.if_supports_color(Stderr, |s| s.yellow()) - ); - - eprintln!("{full_summary}"); - - return Ok(()); - } else { - eprintln!( - "\n{}", - "Summary" - .if_supports_color(Stderr, |s| s.purple()) - .if_supports_color(Stderr, |s| s.bold()) - ); - - let warning_text = format!("{warning_count} warning{plural}"); - - eprintln!( - " 0 errors, {}", - warning_text.if_supports_color(Stderr, |s| s.yellow()), - ); + .if_supports_color(Stderr, |s| s.bold()), + self.error_count, + if self.error_count == 1 { + "error" + } else { + "errors" + } + .if_supports_color(Stderr, |s| s.red()), + self.warning_count, + if self.warning_count == 1 { + "warning" + } else { + "warnings" + } + .if_supports_color(Stderr, |s| s.yellow()), + )) } - - if warning_count > 0 && deny { - return Ok(()); - } - - Ok(()) } #[derive(Debug, Default, Clone, Copy)] diff --git a/crates/aiken/src/main.rs b/crates/aiken/src/main.rs index 777766c8..7ca09dbe 100644 --- a/crates/aiken/src/main.rs +++ b/crates/aiken/src/main.rs @@ -5,13 +5,13 @@ use aiken::cmd::{ tx, uplc, Cmd, }; use aiken_project::{config, pretty}; - use owo_colors::OwoColorize; +use std::process; -fn main() -> miette::Result<()> { +fn main() { panic_handler(); - match Cmd::default() { + let result = match Cmd::default() { Cmd::New(args) => new::exec(args), Cmd::Fmt(args) => fmt::exec(args), Cmd::Build(args) => build::exec(args), @@ -25,6 +25,11 @@ fn main() -> miette::Result<()> { Cmd::Tx(sub_cmd) => tx::exec(sub_cmd), Cmd::Uplc(sub_cmd) => uplc::exec(sub_cmd), Cmd::Completion(sub_cmd) => completion::exec(sub_cmd), + }; + + match result { + Ok(()) => (), + Err(_) => process::exit(1), } } From 6c039708c3aa89707760a2bf81b6211236af5847 Mon Sep 17 00:00:00 2001 From: KtorZ Date: Sat, 25 Nov 2023 14:48:22 +0100 Subject: [PATCH 22/25] Rework 'watch_project' to reuse 'with_project' Also removed the 'clear' flag to do it by default instead of clogging the terminal view. This now works pretty nicely, and the logic is back under `aiken_project`. --- crates/aiken-project/src/telemetry.rs | 344 +++++++++++++++- crates/aiken-project/src/watch.rs | 140 ++++++- crates/aiken/src/cmd/blueprint/address.rs | 4 +- crates/aiken/src/cmd/blueprint/apply.rs | 2 +- crates/aiken/src/cmd/blueprint/hash.rs | 4 +- crates/aiken/src/cmd/blueprint/policy.rs | 4 +- crates/aiken/src/cmd/build.rs | 5 +- crates/aiken/src/cmd/check.rs | 74 +--- crates/aiken/src/cmd/docs.rs | 3 +- crates/aiken/src/lib.rs | 458 ---------------------- crates/aiken/src/main.rs | 6 +- 11 files changed, 484 insertions(+), 560 deletions(-) delete mode 100644 crates/aiken/src/lib.rs diff --git a/crates/aiken-project/src/telemetry.rs b/crates/aiken-project/src/telemetry.rs index 97b23f5c..3440fbe3 100644 --- a/crates/aiken-project/src/telemetry.rs +++ b/crates/aiken-project/src/telemetry.rs @@ -1,5 +1,11 @@ +use crate::pretty; use crate::script::EvalInfo; -use std::{fmt::Display, path::PathBuf}; +use owo_colors::{ + OwoColorize, + Stream::{self, Stderr}, +}; +use std::{collections::BTreeMap, fmt::Display, path::PathBuf}; +use uplc::machine::cost_model::ExBudget; pub trait EventListener { fn handle_event(&self, _event: Event) {} @@ -64,3 +70,339 @@ impl Display for DownloadSource { } } } + +#[derive(Debug, Default, Clone, Copy)] +pub struct Terminal; + +impl EventListener for Terminal { + fn handle_event(&self, event: Event) { + match event { + Event::StartingCompilation { + name, + version, + root, + } => { + eprintln!( + "{} {} {} ({})", + " Compiling" + .if_supports_color(Stderr, |s| s.bold()) + .if_supports_color(Stderr, |s| s.purple()), + name.if_supports_color(Stderr, |s| s.bold()), + version, + root.display() + .if_supports_color(Stderr, |s| s.bright_blue()) + ); + } + Event::BuildingDocumentation { + name, + version, + root, + } => { + eprintln!( + "{} {} {} ({})", + " Generating documentation" + .if_supports_color(Stderr, |s| s.bold()) + .if_supports_color(Stderr, |s| s.purple()), + name.if_supports_color(Stderr, |s| s.bold()), + version, + root.to_str() + .unwrap_or("") + .if_supports_color(Stderr, |s| s.bright_blue()) + ); + } + Event::WaitingForBuildDirLock => { + eprintln!( + "{}", + "Waiting for build directory lock ..." + .if_supports_color(Stderr, |s| s.bold()) + .if_supports_color(Stderr, |s| s.purple()) + ); + } + Event::DumpingUPLC { path } => { + eprintln!( + "{} {} ({})", + " Exporting" + .if_supports_color(Stderr, |s| s.bold()) + .if_supports_color(Stderr, |s| s.purple()), + "UPLC".if_supports_color(Stderr, |s| s.bold()), + path.display() + .if_supports_color(Stderr, |s| s.bright_blue()) + ); + } + Event::GeneratingBlueprint { path } => { + eprintln!( + "{} {} ({})", + " Generating" + .if_supports_color(Stderr, |s| s.bold()) + .if_supports_color(Stderr, |s| s.purple()), + "project's blueprint".if_supports_color(Stderr, |s| s.bold()), + path.display() + .if_supports_color(Stderr, |s| s.bright_blue()) + ); + } + Event::GeneratingDocFiles { output_path } => { + eprintln!( + "{} in {}", + " Generating documentation files" + .if_supports_color(Stderr, |s| s.bold()) + .if_supports_color(Stderr, |s| s.purple()), + output_path + .to_str() + .unwrap_or("") + .if_supports_color(Stderr, |s| s.bright_blue()) + ); + } + Event::GeneratingUPLCFor { name, path } => { + eprintln!( + "{} {}.{{{}}}", + " Generating UPLC for" + .if_supports_color(Stderr, |s| s.bold()) + .if_supports_color(Stderr, |s| s.purple()), + path.to_str() + .unwrap_or("") + .if_supports_color(Stderr, |s| s.blue()), + name.if_supports_color(Stderr, |s| s.bright_blue()), + ); + } + Event::EvaluatingFunction { results } => { + eprintln!( + "{}\n", + " Evaluating function ..." + .if_supports_color(Stderr, |s| s.bold()) + .if_supports_color(Stderr, |s| s.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, Stderr)) + } + } + Event::RunningTests => { + eprintln!( + "{} {}\n", + " Testing" + .if_supports_color(Stderr, |s| s.bold()) + .if_supports_color(Stderr, |s| s.purple()), + "...".if_supports_color(Stderr, |s| s.bold()) + ); + } + Event::FinishedTests { tests } => { + let (max_mem, max_cpu) = find_max_execution_units(&tests); + + for (module, infos) in &group_by_module(&tests) { + let title = module + .if_supports_color(Stderr, |s| s.bold()) + .if_supports_color(Stderr, |s| s.blue()) + .to_string(); + + let tests = infos + .iter() + .map(|eval_info| fmt_test(eval_info, max_mem, max_cpu, true)) + .collect::>() + .join("\n"); + + let summary = fmt_test_summary(infos, true); + + eprintln!( + "{}\n", + pretty::indent( + &pretty::open_box(&title, &tests, &summary, |border| border + .if_supports_color(Stderr, |s| s.bright_black()) + .to_string()), + 4 + ) + ); + } + } + Event::ResolvingPackages { name } => { + eprintln!( + "{} {}", + " Resolving" + .if_supports_color(Stderr, |s| s.bold()) + .if_supports_color(Stderr, |s| s.purple()), + name.if_supports_color(Stderr, |s| s.bold()) + ) + } + Event::PackageResolveFallback { name } => { + eprintln!( + "{} {}\n ↳ You're seeing this message because the package version is unpinned and the network is not accessible.", + " Using" + .if_supports_color(Stderr, |s| s.bold()) + .if_supports_color(Stderr, |s| s.yellow()), + format!("uncertain local version for {name}") + .if_supports_color(Stderr, |s| s.yellow()) + ) + } + Event::PackagesDownloaded { + start, + count, + source, + } => { + let elapsed = format!("{:.2}s", start.elapsed().as_millis() as f32 / 1000.); + + let msg = match count { + 1 => format!("1 package in {elapsed}"), + _ => format!("{count} packages in {elapsed}"), + }; + + eprintln!( + "{} {} from {source}", + match source { + DownloadSource::Network => " Downloaded", + DownloadSource::Cache => " Fetched", + } + .if_supports_color(Stderr, |s| s.bold()) + .if_supports_color(Stderr, |s| s.purple()), + msg.if_supports_color(Stderr, |s| s.bold()) + ) + } + Event::ResolvingVersions => { + eprintln!( + "{}", + " Resolving dependencies" + .if_supports_color(Stderr, |s| s.bold()) + .if_supports_color(Stderr, |s| s.purple()), + ) + } + } + } +} + +fn fmt_test(eval_info: &EvalInfo, max_mem: usize, max_cpu: usize, styled: bool) -> String { + let EvalInfo { + success, + script, + spent_budget, + logs, + .. + } = 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, " "); + + let test = format!( + "{status} [mem: {mem_unit}, cpu: {cpu_unit}] {module}", + status = if *success { + pretty::style_if(styled, "PASS".to_string(), |s| { + s.if_supports_color(Stderr, |s| s.bold()) + .if_supports_color(Stderr, |s| s.green()) + .to_string() + }) + } else { + pretty::style_if(styled, "FAIL".to_string(), |s| { + s.if_supports_color(Stderr, |s| s.bold()) + .if_supports_color(Stderr, |s| s.red()) + .to_string() + }) + }, + mem_unit = pretty::style_if(styled, mem_pad, |s| s + .if_supports_color(Stderr, |s| s.cyan()) + .to_string()), + cpu_unit = pretty::style_if(styled, cpu_pad, |s| s + .if_supports_color(Stderr, |s| s.cyan()) + .to_string()), + module = pretty::style_if(styled, script.name.clone(), |s| s + .if_supports_color(Stderr, |s| s.bright_blue()) + .to_string()), + ); + + let logs = if logs.is_empty() { + String::new() + } else { + logs.iter() + .map(|line| { + format!( + "{arrow} {styled_line}", + arrow = "↳".if_supports_color(Stderr, |s| s.bright_yellow()), + styled_line = line.if_supports_color(Stderr, |s| s.bright_black()) + ) + }) + .collect::>() + .join("\n") + }; + + if logs.is_empty() { + test + } else { + [test, logs].join("\n") + } +} + +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 + .if_supports_color(Stderr, |s| s.bold()) + .to_string()), + pretty::style_if(styled, format!("{n_passed} passed"), |s| s + .if_supports_color(Stderr, |s| s.bright_green()) + .if_supports_color(Stderr, |s| s.bold()) + .to_string()), + pretty::style_if(styled, format!("{n_failed} failed"), |s| s + .if_supports_color(Stderr, |s| s.bright_red()) + .if_supports_color(Stderr, |s| s.bold()) + .to_string()), + ) +} + +fn fmt_eval(eval_info: &EvalInfo, max_mem: usize, max_cpu: usize, stream: Stream) -> String { + let EvalInfo { + output, + script, + spent_budget, + .. + } = eval_info; + + let ExBudget { mem, cpu } = spent_budget; + + format!( + " {}::{} [mem: {}, cpu: {}]\n │\n ╰─▶ {}", + script.module.if_supports_color(stream, |s| s.blue()), + script.name.if_supports_color(stream, |s| s.bright_blue()), + pretty::pad_left(mem.to_string(), max_mem, " "), + pretty::pad_left(cpu.to_string(), max_cpu, " "), + output + .as_ref() + .map(|x| format!("{x}")) + .unwrap_or_else(|| "Error.".to_string()), + ) +} + +fn group_by_module(infos: &Vec) -> BTreeMap> { + let mut modules = BTreeMap::new(); + for eval_info in infos { + let xs: &mut Vec<&EvalInfo> = modules.entry(eval_info.script.module.clone()).or_default(); + xs.push(eval_info); + } + modules +} + +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()) +} diff --git a/crates/aiken-project/src/watch.rs b/crates/aiken-project/src/watch.rs index 61dda460..f3709ca5 100644 --- a/crates/aiken-project/src/watch.rs +++ b/crates/aiken-project/src/watch.rs @@ -1,14 +1,57 @@ -use miette::IntoDiagnostic; +use crate::{telemetry::Terminal, Project}; +use miette::{Diagnostic, IntoDiagnostic}; use notify::{Event, RecursiveMode, Watcher}; +use owo_colors::{OwoColorize, Stream::Stderr}; use std::{ collections::VecDeque, env, ffi::OsStr, - path::PathBuf, + fmt::{self, Display}, + path::Path, sync::{Arc, Mutex}, }; -use crate::{telemetry, Project}; +#[derive(Debug, Diagnostic, thiserror::Error)] +enum ExitFailure { + #[error("")] + ExitFailure, +} + +impl ExitFailure { + fn into_report() -> miette::Report { + ExitFailure::ExitFailure.into() + } +} + +struct Summary { + warning_count: usize, + error_count: usize, +} + +impl Display for Summary { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + f.write_str(&format!( + "{}\n {} {}, {} {}", + "Summary" + .if_supports_color(Stderr, |s| s.purple()) + .if_supports_color(Stderr, |s| s.bold()), + self.error_count, + if self.error_count == 1 { + "error" + } else { + "errors" + } + .if_supports_color(Stderr, |s| s.red()), + self.warning_count, + if self.warning_count == 1 { + "warning" + } else { + "warnings" + } + .if_supports_color(Stderr, |s| s.yellow()), + )) + } +} /// A default filter for file events that catches the most relevant "source" changes pub fn default_filter(evt: &Event) -> bool { @@ -30,6 +73,65 @@ pub fn default_filter(evt: &Event) -> bool { } } +pub fn with_project(directory: Option<&Path>, deny: bool, mut action: A) -> miette::Result<()> +where + A: FnMut(&mut Project) -> Result<(), Vec>, +{ + let project_path = if let Some(d) = directory { + d.to_path_buf() + } else { + env::current_dir().into_diagnostic()? + }; + + let mut project = match Project::new(project_path, Terminal) { + Ok(p) => Ok(p), + Err(e) => { + e.report(); + Err(ExitFailure::into_report()) + } + }?; + + let build_result = action(&mut project); + + let warnings = project.warnings(); + + let warning_count = warnings.len(); + + for warning in &warnings { + warning.report() + } + + if let Err(errs) = build_result { + for err in &errs { + err.report() + } + + eprintln!( + "\n{}", + Summary { + warning_count, + error_count: errs.len(), + } + ); + + return Err(ExitFailure::into_report()); + } else { + eprintln!( + "\n{}", + Summary { + error_count: 0, + warning_count + } + ); + } + + if warning_count > 0 && deny { + Err(ExitFailure::into_report()) + } else { + Ok(()) + } +} + /// Run a function each time a file in the project changes /// /// ```text @@ -41,19 +143,19 @@ pub fn default_filter(evt: &Event) -> bool { /// Ok(()) /// }); /// ``` -pub fn watch_project( - directory: Option, - events: T, +pub fn watch_project( + directory: Option<&Path>, filter: F, debounce: u32, mut action: A, ) -> miette::Result<()> where - T: Copy + telemetry::EventListener, F: Fn(&Event) -> bool, - A: FnMut(&mut Project) -> Result<(), Vec>, + A: FnMut(&mut Project) -> Result<(), Vec>, { - let project_path = directory.unwrap_or(env::current_dir().into_diagnostic()?); + let project_path = directory + .map(|p| p.to_path_buf()) + .unwrap_or(env::current_dir().into_diagnostic()?); // Set up a queue for events, primarily so we can debounce on related events let queue = Arc::new(Mutex::new(VecDeque::new())); @@ -108,18 +210,14 @@ where // If we have an event that survived the filter, then we can construct the project and invoke the action if latest.is_some() { - let mut project = match Project::new(project_path.clone(), events) { - Ok(p) => p, - Err(e) => { - // TODO: what should we actually do here? - e.report(); - return Err(miette::Report::msg("??")); - } - }; - - // Invoke the action, and abort on an error - // TODO: what should we actually do with the error here? - action(&mut project).or(Err(miette::Report::msg("??")))?; + print!("{esc}c", esc = 27 as char); + println!( + "{} ...", + " Watching" + .if_supports_color(Stderr, |s| s.bold()) + .if_supports_color(Stderr, |s| s.purple()), + ); + with_project(directory, false, &mut action).unwrap_or(()) } } } diff --git a/crates/aiken/src/cmd/blueprint/address.rs b/crates/aiken/src/cmd/blueprint/address.rs index 7f7ebbe5..b90aef17 100644 --- a/crates/aiken/src/cmd/blueprint/address.rs +++ b/crates/aiken/src/cmd/blueprint/address.rs @@ -1,5 +1,5 @@ -use crate::with_project; use aiken_lang::ast::Tracing; +use aiken_project::watch::with_project; use std::path::PathBuf; /// Compute a validator's address. @@ -34,7 +34,7 @@ pub fn exec( rebuild, }: Args, ) -> miette::Result<()> { - with_project(directory, false, |p| { + with_project(directory.as_deref(), false, |p| { if rebuild { p.build(false, Tracing::NoTraces)?; } diff --git a/crates/aiken/src/cmd/blueprint/apply.rs b/crates/aiken/src/cmd/blueprint/apply.rs index e8555f44..b86418f5 100644 --- a/crates/aiken/src/cmd/blueprint/apply.rs +++ b/crates/aiken/src/cmd/blueprint/apply.rs @@ -1,4 +1,3 @@ -use crate::with_project; use aiken_project::{ blueprint::{ self, @@ -7,6 +6,7 @@ use aiken_project::{ }, error::Error, pretty::multiline, + watch::with_project, }; use inquire; use num_bigint::BigInt; diff --git a/crates/aiken/src/cmd/blueprint/hash.rs b/crates/aiken/src/cmd/blueprint/hash.rs index 95471be8..e4a8840b 100644 --- a/crates/aiken/src/cmd/blueprint/hash.rs +++ b/crates/aiken/src/cmd/blueprint/hash.rs @@ -1,5 +1,5 @@ -use crate::with_project; use aiken_lang::ast::Tracing; +use aiken_project::watch::with_project; use std::path::PathBuf; /// Compute a validator's hash @@ -29,7 +29,7 @@ pub fn exec( rebuild, }: Args, ) -> miette::Result<()> { - with_project(directory, false, |p| { + with_project(directory.as_deref(), false, |p| { if rebuild { p.build(false, Tracing::NoTraces)?; } diff --git a/crates/aiken/src/cmd/blueprint/policy.rs b/crates/aiken/src/cmd/blueprint/policy.rs index 549236f7..f6a90f31 100644 --- a/crates/aiken/src/cmd/blueprint/policy.rs +++ b/crates/aiken/src/cmd/blueprint/policy.rs @@ -1,5 +1,5 @@ -use crate::with_project; use aiken_lang::ast::Tracing; +use aiken_project::watch::with_project; use std::path::PathBuf; /// Compute a minting scripts Policy ID @@ -29,7 +29,7 @@ pub fn exec( rebuild, }: Args, ) -> miette::Result<()> { - with_project(directory, false, |p| { + with_project(directory.as_deref(), false, |p| { if rebuild { p.build(false, Tracing::NoTraces)?; } diff --git a/crates/aiken/src/cmd/build.rs b/crates/aiken/src/cmd/build.rs index 898c0796..92007a6a 100644 --- a/crates/aiken/src/cmd/build.rs +++ b/crates/aiken/src/cmd/build.rs @@ -1,3 +1,4 @@ +use aiken_project::watch::with_project; use std::path::PathBuf; #[derive(clap::Args)] @@ -27,5 +28,7 @@ pub fn exec( keep_traces, }: Args, ) -> miette::Result<()> { - crate::with_project(directory, deny, |p| p.build(uplc, keep_traces.into())) + with_project(directory.as_deref(), deny, |p| { + p.build(uplc, keep_traces.into()) + }) } diff --git a/crates/aiken/src/cmd/check.rs b/crates/aiken/src/cmd/check.rs index 59e7c5fb..63fc4068 100644 --- a/crates/aiken/src/cmd/check.rs +++ b/crates/aiken/src/cmd/check.rs @@ -1,10 +1,6 @@ +use aiken_project::watch::{self, watch_project, with_project}; use std::path::PathBuf; -use aiken_project::watch; -use owo_colors::{OwoColorize, Stream::Stderr}; - -use crate::Terminal; - #[derive(clap::Args)] /// Type-check an Aiken project pub struct Args { @@ -27,10 +23,6 @@ pub struct Args { #[clap(long)] watch: bool, - /// When enabled, clear the screen before running - #[clap(long)] - clear: bool, - /// Only run tests if they match any of these strings. /// You can match a module with `-m aiken/list` or `-m list`. /// You can match a test with `-m "aiken/list.{map}"` or `-m "aiken/option.{flatten_1}"` @@ -57,77 +49,21 @@ pub fn exec( exact_match, no_traces, watch, - clear, .. }: Args, ) -> miette::Result<()> { if watch { - watch::watch_project(directory, Terminal, watch::default_filter, 500, |p| { - if clear { - println!("{esc}c", esc = 27 as char); - } - let build_result = p.check( + watch_project(directory.as_deref(), watch::default_filter, 500, |p| { + p.check( skip_tests, match_tests.clone(), debug, exact_match, (!no_traces).into(), - ); - - let warnings = p.warnings(); - - let warning_count = warnings.len(); - - for warning in &warnings { - warning.report() - } - - let plural = if warning_count == 1 { "" } else { "s" }; - - if let Err(errs) = build_result { - for err in &errs { - err.report() - } - - eprintln!( - "\n{}", - "Summary" - .if_supports_color(Stderr, |s| s.purple()) - .if_supports_color(Stderr, |s| s.bold()) - ); - - let warning_text = format!("{warning_count} warning{plural}"); - - let plural = if errs.len() == 1 { "" } else { "s" }; - - let error_text = format!("{} error{}", errs.len(), plural); - - let full_summary = format!( - " {}, {}", - error_text.if_supports_color(Stderr, |s| s.red()), - warning_text.if_supports_color(Stderr, |s| s.yellow()) - ); - - eprintln!("{full_summary}"); - } else { - eprintln!( - "\n{}", - "Summary" - .if_supports_color(Stderr, |s| s.purple()) - .if_supports_color(Stderr, |s| s.bold()) - ); - - let warning_text = format!("{warning_count} warning{plural}"); - - eprintln!( - " 0 errors, {}", - warning_text.if_supports_color(Stderr, |s| s.yellow()), - ); - } - Ok(()) + ) }) } else { - crate::with_project(directory, deny, |p| { + with_project(directory.as_deref(), deny, |p| { p.check( skip_tests, match_tests.clone(), diff --git a/crates/aiken/src/cmd/docs.rs b/crates/aiken/src/cmd/docs.rs index 9b3e94a7..f03d2e7b 100644 --- a/crates/aiken/src/cmd/docs.rs +++ b/crates/aiken/src/cmd/docs.rs @@ -1,3 +1,4 @@ +use aiken_project::watch::with_project; use std::path::PathBuf; #[derive(clap::Args)] @@ -22,5 +23,5 @@ pub fn exec( destination, }: Args, ) -> miette::Result<()> { - crate::with_project(directory, deny, |p| p.docs(destination.clone())) + with_project(directory.as_deref(), deny, |p| p.docs(destination.clone())) } diff --git a/crates/aiken/src/lib.rs b/crates/aiken/src/lib.rs deleted file mode 100644 index a2627f4c..00000000 --- a/crates/aiken/src/lib.rs +++ /dev/null @@ -1,458 +0,0 @@ -use aiken_project::{ - pretty, - script::EvalInfo, - telemetry::{self, DownloadSource}, - Project, -}; -use miette::Diagnostic; -use miette::IntoDiagnostic; -use owo_colors::{ - OwoColorize, - Stream::{self, Stderr}, -}; -use std::{ - collections::BTreeMap, - env, - fmt::{self, Display}, - path::PathBuf, -}; -use uplc::machine::cost_model::ExBudget; - -pub mod cmd; - -#[derive(Debug, Diagnostic, thiserror::Error)] -enum ExitFailure { - #[error("")] - ExitFailure, -} - -impl ExitFailure { - fn into_report() -> miette::Report { - ExitFailure::ExitFailure.into() - } -} - -pub fn with_project(directory: Option, deny: bool, mut action: A) -> miette::Result<()> -where - A: FnMut(&mut Project) -> Result<(), Vec>, -{ - let project_path = if let Some(d) = directory { - d - } else { - env::current_dir().into_diagnostic()? - }; - - let mut project = match Project::new(project_path, Terminal) { - Ok(p) => Ok(p), - Err(e) => { - e.report(); - Err(ExitFailure::into_report()) - } - }?; - - let build_result = action(&mut project); - - let warnings = project.warnings(); - - let warning_count = warnings.len(); - - for warning in &warnings { - warning.report() - } - - if let Err(errs) = build_result { - for err in &errs { - err.report() - } - - eprintln!( - "\n{}", - Summary { - warning_count, - error_count: errs.len(), - } - ); - - return Err(ExitFailure::into_report()); - } else { - eprintln!( - "\n{}", - Summary { - error_count: 0, - warning_count - } - ); - } - - if warning_count > 0 && deny { - Err(ExitFailure::into_report()) - } else { - Ok(()) - } -} - -struct Summary { - warning_count: usize, - error_count: usize, -} - -impl Display for Summary { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - f.write_str(&format!( - "{}\n {} {}, {} {}", - "Summary" - .if_supports_color(Stderr, |s| s.purple()) - .if_supports_color(Stderr, |s| s.bold()), - self.error_count, - if self.error_count == 1 { - "error" - } else { - "errors" - } - .if_supports_color(Stderr, |s| s.red()), - self.warning_count, - if self.warning_count == 1 { - "warning" - } else { - "warnings" - } - .if_supports_color(Stderr, |s| s.yellow()), - )) - } -} - -#[derive(Debug, Default, Clone, Copy)] -pub struct Terminal; - -impl telemetry::EventListener for Terminal { - fn handle_event(&self, event: telemetry::Event) { - match event { - telemetry::Event::StartingCompilation { - name, - version, - root, - } => { - eprintln!( - "{} {} {} ({})", - " Compiling" - .if_supports_color(Stderr, |s| s.bold()) - .if_supports_color(Stderr, |s| s.purple()), - name.if_supports_color(Stderr, |s| s.bold()), - version, - root.display() - .if_supports_color(Stderr, |s| s.bright_blue()) - ); - } - telemetry::Event::BuildingDocumentation { - name, - version, - root, - } => { - eprintln!( - "{} {} {} ({})", - " Generating documentation" - .if_supports_color(Stderr, |s| s.bold()) - .if_supports_color(Stderr, |s| s.purple()), - name.if_supports_color(Stderr, |s| s.bold()), - version, - root.to_str() - .unwrap_or("") - .if_supports_color(Stderr, |s| s.bright_blue()) - ); - } - telemetry::Event::WaitingForBuildDirLock => { - eprintln!( - "{}", - "Waiting for build directory lock ..." - .if_supports_color(Stderr, |s| s.bold()) - .if_supports_color(Stderr, |s| s.purple()) - ); - } - telemetry::Event::DumpingUPLC { path } => { - eprintln!( - "{} {} ({})", - " Exporting" - .if_supports_color(Stderr, |s| s.bold()) - .if_supports_color(Stderr, |s| s.purple()), - "UPLC".if_supports_color(Stderr, |s| s.bold()), - path.display() - .if_supports_color(Stderr, |s| s.bright_blue()) - ); - } - telemetry::Event::GeneratingBlueprint { path } => { - eprintln!( - "{} {} ({})", - " Generating" - .if_supports_color(Stderr, |s| s.bold()) - .if_supports_color(Stderr, |s| s.purple()), - "project's blueprint".if_supports_color(Stderr, |s| s.bold()), - path.display() - .if_supports_color(Stderr, |s| s.bright_blue()) - ); - } - telemetry::Event::GeneratingDocFiles { output_path } => { - eprintln!( - "{} in {}", - " Generating documentation files" - .if_supports_color(Stderr, |s| s.bold()) - .if_supports_color(Stderr, |s| s.purple()), - output_path - .to_str() - .unwrap_or("") - .if_supports_color(Stderr, |s| s.bright_blue()) - ); - } - telemetry::Event::GeneratingUPLCFor { name, path } => { - eprintln!( - "{} {}.{{{}}}", - " Generating UPLC for" - .if_supports_color(Stderr, |s| s.bold()) - .if_supports_color(Stderr, |s| s.purple()), - path.to_str() - .unwrap_or("") - .if_supports_color(Stderr, |s| s.blue()), - name.if_supports_color(Stderr, |s| s.bright_blue()), - ); - } - telemetry::Event::EvaluatingFunction { results } => { - eprintln!( - "{}\n", - " Evaluating function ..." - .if_supports_color(Stderr, |s| s.bold()) - .if_supports_color(Stderr, |s| s.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, Stderr)) - } - } - telemetry::Event::RunningTests => { - eprintln!( - "{} {}\n", - " Testing" - .if_supports_color(Stderr, |s| s.bold()) - .if_supports_color(Stderr, |s| s.purple()), - "...".if_supports_color(Stderr, |s| s.bold()) - ); - } - telemetry::Event::FinishedTests { tests } => { - let (max_mem, max_cpu) = find_max_execution_units(&tests); - - for (module, infos) in &group_by_module(&tests) { - let title = module - .if_supports_color(Stderr, |s| s.bold()) - .if_supports_color(Stderr, |s| s.blue()) - .to_string(); - - let tests = infos - .iter() - .map(|eval_info| fmt_test(eval_info, max_mem, max_cpu, true)) - .collect::>() - .join("\n"); - - let summary = fmt_test_summary(infos, true); - - eprintln!( - "{}\n", - pretty::indent( - &pretty::open_box(&title, &tests, &summary, |border| border - .if_supports_color(Stderr, |s| s.bright_black()) - .to_string()), - 4 - ) - ); - } - } - telemetry::Event::ResolvingPackages { name } => { - eprintln!( - "{} {}", - " Resolving" - .if_supports_color(Stderr, |s| s.bold()) - .if_supports_color(Stderr, |s| s.purple()), - name.if_supports_color(Stderr, |s| s.bold()) - ) - } - telemetry::Event::PackageResolveFallback { name } => { - eprintln!( - "{} {}\n ↳ You're seeing this message because the package version is unpinned and the network is not accessible.", - " Using" - .if_supports_color(Stderr, |s| s.bold()) - .if_supports_color(Stderr, |s| s.yellow()), - format!("uncertain local version for {name}") - .if_supports_color(Stderr, |s| s.yellow()) - ) - } - telemetry::Event::PackagesDownloaded { - start, - count, - source, - } => { - let elapsed = format!("{:.2}s", start.elapsed().as_millis() as f32 / 1000.); - - let msg = match count { - 1 => format!("1 package in {elapsed}"), - _ => format!("{count} packages in {elapsed}"), - }; - - eprintln!( - "{} {} from {source}", - match source { - DownloadSource::Network => " Downloaded", - DownloadSource::Cache => " Fetched", - } - .if_supports_color(Stderr, |s| s.bold()) - .if_supports_color(Stderr, |s| s.purple()), - msg.if_supports_color(Stderr, |s| s.bold()) - ) - } - telemetry::Event::ResolvingVersions => { - eprintln!( - "{}", - " Resolving dependencies" - .if_supports_color(Stderr, |s| s.bold()) - .if_supports_color(Stderr, |s| s.purple()), - ) - } - } - } -} - -fn fmt_test(eval_info: &EvalInfo, max_mem: usize, max_cpu: usize, styled: bool) -> String { - let EvalInfo { - success, - script, - spent_budget, - logs, - .. - } = 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, " "); - - let test = format!( - "{status} [mem: {mem_unit}, cpu: {cpu_unit}] {module}", - status = if *success { - pretty::style_if(styled, "PASS".to_string(), |s| { - s.if_supports_color(Stderr, |s| s.bold()) - .if_supports_color(Stderr, |s| s.green()) - .to_string() - }) - } else { - pretty::style_if(styled, "FAIL".to_string(), |s| { - s.if_supports_color(Stderr, |s| s.bold()) - .if_supports_color(Stderr, |s| s.red()) - .to_string() - }) - }, - mem_unit = pretty::style_if(styled, mem_pad, |s| s - .if_supports_color(Stderr, |s| s.cyan()) - .to_string()), - cpu_unit = pretty::style_if(styled, cpu_pad, |s| s - .if_supports_color(Stderr, |s| s.cyan()) - .to_string()), - module = pretty::style_if(styled, script.name.clone(), |s| s - .if_supports_color(Stderr, |s| s.bright_blue()) - .to_string()), - ); - - let logs = if logs.is_empty() { - String::new() - } else { - logs.iter() - .map(|line| { - format!( - "{arrow} {styled_line}", - arrow = "↳".if_supports_color(Stderr, |s| s.bright_yellow()), - styled_line = line.if_supports_color(Stderr, |s| s.bright_black()) - ) - }) - .collect::>() - .join("\n") - }; - - if logs.is_empty() { - test - } else { - [test, logs].join("\n") - } -} - -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 - .if_supports_color(Stderr, |s| s.bold()) - .to_string()), - pretty::style_if(styled, format!("{n_passed} passed"), |s| s - .if_supports_color(Stderr, |s| s.bright_green()) - .if_supports_color(Stderr, |s| s.bold()) - .to_string()), - pretty::style_if(styled, format!("{n_failed} failed"), |s| s - .if_supports_color(Stderr, |s| s.bright_red()) - .if_supports_color(Stderr, |s| s.bold()) - .to_string()), - ) -} - -fn fmt_eval(eval_info: &EvalInfo, max_mem: usize, max_cpu: usize, stream: Stream) -> String { - let EvalInfo { - output, - script, - spent_budget, - .. - } = eval_info; - - let ExBudget { mem, cpu } = spent_budget; - - format!( - " {}::{} [mem: {}, cpu: {}]\n │\n ╰─▶ {}", - script.module.if_supports_color(stream, |s| s.blue()), - script.name.if_supports_color(stream, |s| s.bright_blue()), - pretty::pad_left(mem.to_string(), max_mem, " "), - pretty::pad_left(cpu.to_string(), max_cpu, " "), - output - .as_ref() - .map(|x| format!("{x}")) - .unwrap_or_else(|| "Error.".to_string()), - ) -} - -fn group_by_module(infos: &Vec) -> BTreeMap> { - let mut modules = BTreeMap::new(); - for eval_info in infos { - let xs: &mut Vec<&EvalInfo> = modules.entry(eval_info.script.module.clone()).or_default(); - xs.push(eval_info); - } - modules -} - -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()) -} diff --git a/crates/aiken/src/main.rs b/crates/aiken/src/main.rs index 7ca09dbe..e57d03a2 100644 --- a/crates/aiken/src/main.rs +++ b/crates/aiken/src/main.rs @@ -1,13 +1,15 @@ -use aiken::cmd::{ +use aiken_project::{config, pretty}; +use cmd::{ blueprint::{self, address}, build, check, completion, docs, fmt, lsp, new, packages::{self, add}, tx, uplc, Cmd, }; -use aiken_project::{config, pretty}; use owo_colors::OwoColorize; use std::process; +mod cmd; + fn main() { panic_handler(); From 7645a9460f68b4e039aaed963731bcf8cbc86426 Mon Sep 17 00:00:00 2001 From: KtorZ Date: Sat, 25 Nov 2023 14:49:56 +0100 Subject: [PATCH 23/25] Display error codes better. This is a *slight* hack / abuse of the code() method as we are now doing a bit of formatting within that function. Yet, we only do so at the very top-level (i.e. project's Error) because we can't actually fiddle with how miette presents errors. --- crates/aiken-project/src/error.rs | 54 +++++++++++++++++++++--------- crates/aiken-project/src/script.rs | 2 +- crates/aiken-project/src/watch.rs | 12 ++++--- 3 files changed, 47 insertions(+), 21 deletions(-) diff --git a/crates/aiken-project/src/error.rs b/crates/aiken-project/src/error.rs index cc0620bb..f75947fc 100644 --- a/crates/aiken-project/src/error.rs +++ b/crates/aiken-project/src/error.rs @@ -262,31 +262,45 @@ impl Diagnostic for Error { } fn code<'a>(&'a self) -> Option> { + fn boxed<'a>(s: Box) -> Box { + Box::new(format!( + " {} {}", + "Error" + .if_supports_color(Stdout, |s| s.red()) + .if_supports_color(Stdout, |s| s.bold()), + format!("{s}").if_supports_color(Stdout, |s| s.red()) + )) + } + match self { - Error::DuplicateModule { .. } => Some(Box::new("aiken::module::duplicate")), + Error::DuplicateModule { .. } => Some(boxed(Box::new("aiken::module::duplicate"))), Error::FileIo { .. } => None, - Error::Blueprint(e) => e.code(), - Error::ImportCycle { .. } => Some(Box::new("aiken::module::cyclical")), - Error::Parse { .. } => Some(Box::new("aiken::parser")), - Error::Type { error, .. } => Some(Box::new(format!( + Error::Blueprint(e) => e.code().map(boxed), + Error::ImportCycle { .. } => Some(boxed(Box::new("aiken::module::cyclical"))), + Error::Parse { .. } => Some(boxed(Box::new("aiken::parser"))), + Error::Type { error, .. } => Some(boxed(Box::new(format!( "aiken::check{}", error.code().map(|s| format!("::{s}")).unwrap_or_default() - ))), + )))), Error::StandardIo(_) => None, Error::MissingManifest { .. } => None, - Error::TomlLoading { .. } => Some(Box::new("aiken::loading::toml")), + Error::TomlLoading { .. } => Some(boxed(Box::new("aiken::loading::toml"))), Error::Format { .. } => None, - Error::TestFailure { path, .. } => Some(Box::new(path.to_str().unwrap_or(""))), + Error::TestFailure { path, .. } => Some(boxed(Box::new(path.to_str().unwrap_or("")))), Error::Http(_) => Some(Box::new("aiken::packages::download")), Error::ZipExtract(_) => None, Error::JoinError(_) => None, - Error::UnknownPackageVersion { .. } => Some(Box::new("aiken::packages::resolve")), - Error::UnableToResolvePackage { .. } => Some(Box::new("aiken::package::download")), + Error::UnknownPackageVersion { .. } => { + Some(boxed(Box::new("aiken::packages::resolve"))) + } + Error::UnableToResolvePackage { .. } => { + Some(boxed(Box::new("aiken::package::download"))) + } Error::Json { .. } => None, Error::MalformedStakeAddress { .. } => None, Error::NoValidatorNotFound { .. } => None, Error::MoreThanOneValidatorFound { .. } => None, - Error::Module(e) => e.code(), + Error::Module(e) => e.code().map(boxed), } } @@ -532,14 +546,24 @@ impl Diagnostic for Warning { } fn code<'a>(&'a self) -> Option> { + fn boxed<'a>(s: Box) -> Box { + Box::new(format!( + " {} {}", + "Warning" + .if_supports_color(Stdout, |s| s.yellow()) + .if_supports_color(Stdout, |s| s.bold()), + format!("{s}").if_supports_color(Stdout, |s| s.yellow()) + )) + } + match self { - Warning::Type { warning, .. } => Some(Box::new(format!( + Warning::Type { warning, .. } => Some(boxed(Box::new(format!( "aiken::check{}", warning.code().map(|s| format!("::{s}")).unwrap_or_default() - ))), - Warning::NoValidators => Some(Box::new("aiken::check")), + )))), + Warning::NoValidators => Some(boxed(Box::new("aiken::check"))), Warning::DependencyAlreadyExists { .. } => { - Some(Box::new("aiken::packages::already_exists")) + Some(boxed(Box::new("aiken::packages::already_exists"))) } } } diff --git a/crates/aiken-project/src/script.rs b/crates/aiken-project/src/script.rs index ce3fc636..9aa4d28d 100644 --- a/crates/aiken-project/src/script.rs +++ b/crates/aiken-project/src/script.rs @@ -81,7 +81,7 @@ impl Display for EvalHint { )), _ => None, } - .ok_or(fmt::Error::default())?; + .ok_or(fmt::Error)?; f.write_str(&msg) } diff --git a/crates/aiken-project/src/watch.rs b/crates/aiken-project/src/watch.rs index f3709ca5..99934ff8 100644 --- a/crates/aiken-project/src/watch.rs +++ b/crates/aiken-project/src/watch.rs @@ -31,7 +31,7 @@ struct Summary { impl Display for Summary { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { f.write_str(&format!( - "{}\n {} {}, {} {}", + " {} {} {}, {} {}", "Summary" .if_supports_color(Stderr, |s| s.purple()) .if_supports_color(Stderr, |s| s.bold()), @@ -41,14 +41,16 @@ impl Display for Summary { } else { "errors" } - .if_supports_color(Stderr, |s| s.red()), + .if_supports_color(Stderr, |s| s.red()) + .if_supports_color(Stderr, |s| s.bold()), self.warning_count, if self.warning_count == 1 { "warning" } else { "warnings" } - .if_supports_color(Stderr, |s| s.yellow()), + .if_supports_color(Stderr, |s| s.yellow()) + .if_supports_color(Stderr, |s| s.bold()), )) } } @@ -107,7 +109,7 @@ where } eprintln!( - "\n{}", + "{}", Summary { warning_count, error_count: errs.len(), @@ -117,7 +119,7 @@ where return Err(ExitFailure::into_report()); } else { eprintln!( - "\n{}", + "{}", Summary { error_count: 0, warning_count From 40c0fa7d77f8940b4dbb6f1c0f7013b647961baa Mon Sep 17 00:00:00 2001 From: KtorZ Date: Sat, 25 Nov 2023 14:52:51 +0100 Subject: [PATCH 24/25] Add --watch flag to the 'build' and 'docs' commands too. --- crates/aiken/src/cmd/build.rs | 19 +++++++++++++++---- crates/aiken/src/cmd/docs.rs | 15 +++++++++++++-- 2 files changed, 28 insertions(+), 6 deletions(-) diff --git a/crates/aiken/src/cmd/build.rs b/crates/aiken/src/cmd/build.rs index 92007a6a..4067ab9e 100644 --- a/crates/aiken/src/cmd/build.rs +++ b/crates/aiken/src/cmd/build.rs @@ -1,4 +1,4 @@ -use aiken_project::watch::with_project; +use aiken_project::watch::{self, watch_project, with_project}; use std::path::PathBuf; #[derive(clap::Args)] @@ -11,6 +11,10 @@ pub struct Args { #[clap(short = 'D', long)] deny: bool, + /// When enabled, re-run the command on file changes instead of exiting + #[clap(short, long)] + watch: bool, + /// Also dump textual uplc #[clap(short, long)] uplc: bool, @@ -24,11 +28,18 @@ pub fn exec( Args { directory, deny, + watch, uplc, keep_traces, }: Args, ) -> miette::Result<()> { - with_project(directory.as_deref(), deny, |p| { - p.build(uplc, keep_traces.into()) - }) + if watch { + watch_project(directory.as_deref(), watch::default_filter, 500, |p| { + p.build(uplc, keep_traces.into()) + }) + } else { + with_project(directory.as_deref(), deny, |p| { + p.build(uplc, keep_traces.into()) + }) + } } diff --git a/crates/aiken/src/cmd/docs.rs b/crates/aiken/src/cmd/docs.rs index f03d2e7b..2bf0be39 100644 --- a/crates/aiken/src/cmd/docs.rs +++ b/crates/aiken/src/cmd/docs.rs @@ -1,4 +1,4 @@ -use aiken_project::watch::with_project; +use aiken_project::watch::{self, watch_project, with_project}; use std::path::PathBuf; #[derive(clap::Args)] @@ -11,6 +11,10 @@ pub struct Args { #[clap(short = 'D', long)] deny: bool, + /// When enabled, re-run the command on file changes instead of exiting + #[clap(short, long)] + watch: bool, + /// Output directory for the documentation #[clap(short = 'o', long)] destination: Option, @@ -20,8 +24,15 @@ pub fn exec( Args { directory, deny, + watch, destination, }: Args, ) -> miette::Result<()> { - with_project(directory.as_deref(), deny, |p| p.docs(destination.clone())) + if watch { + watch_project(directory.as_deref(), watch::default_filter, 500, |p| { + p.docs(destination.clone()) + }) + } else { + with_project(directory.as_deref(), deny, |p| p.docs(destination.clone())) + } } From a23bc32fa25858650b02b39a34146e8d0fdabe5c Mon Sep 17 00:00:00 2001 From: KtorZ Date: Sat, 25 Nov 2023 15:14:00 +0100 Subject: [PATCH 25/25] Fill-in CHANGELOG. --- CHANGELOG.md | 16 +++++++- Cargo.lock | 111 +++++++++++++++++++++++++++++++++++++++++++++------ 2 files changed, 114 insertions(+), 13 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8951bfef..5ab2b734 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,20 @@ # Changelog -## v1.0.20-alpha - 10/25/2023 +## v1.0.21-alpha - unreleased + +### Added + +- **aiken**: `--watch` flag on the `build`, `check` and `docs` commands to automatically watch and re-execute the command on file changes. + +### Changed + +N/A + +### Fixed + +N/A + +## v1.0.20-alpha - 2023-10-25 ### Added diff --git a/Cargo.lock b/Cargo.lock index 2bd01ac4..53a3aa9a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -141,6 +141,7 @@ dependencies = [ "itertools", "miette", "minicbor", + "notify", "owo-colors", "pallas", "pallas-traverse", @@ -360,6 +361,12 @@ version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" +[[package]] +name = "bitflags" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "327762f6e5a765692301e5bb513e0d9fef63be86bbc14528052b1cd3e6f03e07" + [[package]] name = "block-buffer" version = "0.10.4" @@ -505,7 +512,7 @@ checksum = "223163f58c9a40c3b0a43e1c4b50a9ce09f007ea2cb1ec258a687945b4b7929f" dependencies = [ "anstream", "anstyle", - "bitflags", + "bitflags 1.3.2", "clap_lex", "strsim", "terminal_size 0.2.6", @@ -662,7 +669,7 @@ version = "0.25.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e64e6c0fbe2c17357405f7c758c1ef960fce08bdfb2c03d88d2a18d7e09c4b67" dependencies = [ - "bitflags", + "bitflags 1.3.2", "crossterm_winapi", "libc", "mio", @@ -866,6 +873,18 @@ dependencies = [ "subtle", ] +[[package]] +name = "filetime" +version = "0.2.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4029edd3e734da6fe05b6cd7bd2960760a616bd2ddd0d59a0124746d6272af0" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall 0.3.5", + "windows-sys 0.48.0", +] + [[package]] name = "fixedbitset" version = "0.4.2" @@ -921,6 +940,15 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "fsevent-sys" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76ee7a02da4d231650c7cea31349b889be2f45ddb3ef3032d2ec8185f6313fd2" +dependencies = [ + "libc", +] + [[package]] name = "fslock" version = "0.2.1" @@ -1054,7 +1082,7 @@ version = "0.17.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7b989d6a7ca95a362cf2cfc5ad688b3a467be1f87e480b8dad07fee8c79b0044" dependencies = [ - "bitflags", + "bitflags 1.3.2", "libc", "libgit2-sys", "log", @@ -1293,13 +1321,33 @@ version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9f2cb48b81b1dc9f39676bf99f5499babfec7cd8fe14307f7b3d747208fb5690" +[[package]] +name = "inotify" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8069d3ec154eb856955c1c0fbffefbf5f3c40a104ec912d4797314c1801abff" +dependencies = [ + "bitflags 1.3.2", + "inotify-sys", + "libc", +] + +[[package]] +name = "inotify-sys" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e05c02b5e89bff3b946cedeca278abc628fe811e604f027c45a8aa3cf793d0eb" +dependencies = [ + "libc", +] + [[package]] name = "inquire" version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c33e7c1ddeb15c9abcbfef6029d8e29f69b52b6d6c891031b88ed91b5065803b" dependencies = [ - "bitflags", + "bitflags 1.3.2", "crossterm", "dyn-clone", "lazy_static", @@ -1414,6 +1462,26 @@ dependencies = [ "signature", ] +[[package]] +name = "kqueue" +version = "1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7447f1ca1b7b563588a205fe93dea8df60fd981423a768bc1c0ded35ed147d0c" +dependencies = [ + "kqueue-sys", + "libc", +] + +[[package]] +name = "kqueue-sys" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed9625ffda8729b85e45cf04090035ac368927b8cebc34898e7c120f52e4838b" +dependencies = [ + "bitflags 1.3.2", + "libc", +] + [[package]] name = "lazy_static" version = "1.4.0" @@ -1505,7 +1573,7 @@ version = "0.94.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b63735a13a1f9cd4f4835223d828ed9c2e35c8c5e61837774399f558b6a1237" dependencies = [ - "bitflags", + "bitflags 1.3.2", "serde", "serde_json", "serde_repr", @@ -1660,6 +1728,25 @@ dependencies = [ "minimal-lexical", ] +[[package]] +name = "notify" +version = "6.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6205bd8bb1e454ad2e27422015fb5e4f2bcc7e08fa8f27058670d208324a4d2d" +dependencies = [ + "bitflags 2.4.1", + "crossbeam-channel", + "filetime", + "fsevent-sys", + "inotify", + "kqueue", + "libc", + "log", + "mio", + "walkdir", + "windows-sys 0.48.0", +] + [[package]] name = "num-bigint" version = "0.4.3" @@ -1728,7 +1815,7 @@ version = "0.10.55" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "345df152bc43501c5eb9e4654ff05f794effb78d4efe3d53abc158baddc0703d" dependencies = [ - "bitflags", + "bitflags 1.3.2", "cfg-if", "foreign-types", "libc", @@ -2069,7 +2156,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4e35c06b98bf36aba164cc17cb25f7e232f5c4aeea73baa14b8a9f0d92dbfa65" dependencies = [ "bit-set", - "bitflags", + "bitflags 1.3.2", "byteorder", "lazy_static", "num-traits", @@ -2097,7 +2184,7 @@ version = "0.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2d9cc634bc78768157b5cbfe988ffcd1dcba95cd2b2f03a88316c08c6d00ed63" dependencies = [ - "bitflags", + "bitflags 1.3.2", "memchr", "unicase", ] @@ -2184,7 +2271,7 @@ version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a" dependencies = [ - "bitflags", + "bitflags 1.3.2", ] [[package]] @@ -2193,7 +2280,7 @@ version = "0.3.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "567664f262709473930a4bf9e51bf2ebf3348f2e748ccc50dea20646858f8f29" dependencies = [ - "bitflags", + "bitflags 1.3.2", ] [[package]] @@ -2283,7 +2370,7 @@ version = "0.37.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "85597d61f83914ddeba6a47b3b8ffe7365107221c2e557ed94426489fefb5f77" dependencies = [ - "bitflags", + "bitflags 1.3.2", "errno", "io-lifetimes", "libc", @@ -2377,7 +2464,7 @@ version = "2.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a332be01508d814fed64bf28f798a146d73792121129962fdf335bb3c49a4254" dependencies = [ - "bitflags", + "bitflags 1.3.2", "core-foundation", "core-foundation-sys", "libc",