diff --git a/CHANGELOG.md b/CHANGELOG.md index 366a34cc..814038cd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ - **aiken-lang**: add support for `mk_cons` and `mk_pair_data` builtins. See [#964](https://github.com/aiken-lang/aiken/issues/964). @KtorZ - **aiken-lang**: pattern-matching on bytearrays is now available. See [#989](https://github.com/aiken-lang/aiken/issues/989). @KtorZ +- **aiken-project**: conditional configuration and environment. See [#937](https://github.com/aiken-lang/aiken/issues/937). @KtorZ ### Changed diff --git a/crates/aiken-lang/src/ast.rs b/crates/aiken-lang/src/ast.rs index ac297986..3f462192 100644 --- a/crates/aiken-lang/src/ast.rs +++ b/crates/aiken-lang/src/ast.rs @@ -21,6 +21,10 @@ pub const BACKPASS_VARIABLE: &str = "_backpass"; pub const CAPTURE_VARIABLE: &str = "_capture"; pub const PIPE_VARIABLE: &str = "_pipe"; +pub const ENV_MODULE: &str = "env"; +pub const CONFIG_MODULE: &str = "config"; +pub const DEFAULT_ENV_MODULE: &str = "default"; + pub type TypedModule = Module; pub type UntypedModule = Module<(), UntypedDefinition>; @@ -28,6 +32,8 @@ pub type UntypedModule = Module<(), UntypedDefinition>; pub enum ModuleKind { Lib, Validator, + Env, + Config, } impl ModuleKind { @@ -38,6 +44,14 @@ impl ModuleKind { pub fn is_lib(&self) -> bool { matches!(self, ModuleKind::Lib) } + + pub fn is_env(&self) -> bool { + matches!(self, ModuleKind::Env) + } + + pub fn is_config(&self) -> bool { + matches!(self, ModuleKind::Config) + } } #[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)] @@ -61,16 +75,18 @@ impl Module { } impl UntypedModule { - pub fn dependencies(&self) -> Vec<(String, Span)> { + pub fn dependencies(&self, env_modules: &[String]) -> Vec { self.definitions() .flat_map(|def| { - if let Definition::Use(Use { - location, module, .. - }) = def - { - Some((module.join("/"), *location)) + if let Definition::Use(Use { module, .. }) = def { + let name = module.join("/"); + if name == ENV_MODULE { + env_modules.to_vec() + } else { + vec![name] + } } else { - None + Vec::new() } }) .collect() @@ -1069,6 +1085,15 @@ impl Annotation { } } + pub fn bytearray(location: Span) -> Self { + Annotation::Constructor { + name: "ByteArray".to_string(), + module: None, + arguments: vec![], + location, + } + } + pub fn data(location: Span) -> Self { Annotation::Constructor { name: "Data".to_string(), diff --git a/crates/aiken-lang/src/format.rs b/crates/aiken-lang/src/format.rs index 74e64122..45bc12a4 100644 --- a/crates/aiken-lang/src/format.rs +++ b/crates/aiken-lang/src/format.rs @@ -25,8 +25,8 @@ use ordinal::Ordinal; use std::rc::Rc; use vec1::Vec1; -const INDENT: isize = 2; -const DOCS_MAX_COLUMNS: isize = 80; +pub const INDENT: isize = 2; +pub const DOCS_MAX_COLUMNS: isize = 80; pub fn pretty(writer: &mut String, module: UntypedModule, extra: ModuleExtra, src: &str) { let intermediate = Intermediate { @@ -130,7 +130,7 @@ impl<'comments> Formatter<'comments> { end != 0 } - fn definitions<'a>(&mut self, definitions: &'a [UntypedDefinition]) -> Document<'a> { + pub fn definitions<'a>(&mut self, definitions: &'a [UntypedDefinition]) -> Document<'a> { let mut has_imports = false; let mut has_declarations = false; let mut imports = Vec::new(); diff --git a/crates/aiken-lang/src/lib.rs b/crates/aiken-lang/src/lib.rs index 2910c3f3..c0ed6894 100644 --- a/crates/aiken-lang/src/lib.rs +++ b/crates/aiken-lang/src/lib.rs @@ -49,6 +49,7 @@ macro_rules! aiken_fn { $module_types, $crate::ast::Tracing::silent(), &mut warnings, + None, ) .unwrap(); diff --git a/crates/aiken-lang/src/tests/check.rs b/crates/aiken-lang/src/tests/check.rs index ecf1c528..906240e7 100644 --- a/crates/aiken-lang/src/tests/check.rs +++ b/crates/aiken-lang/src/tests/check.rs @@ -38,6 +38,7 @@ fn check_module( &module_types, Tracing::All(TraceLevel::Verbose), &mut warnings, + None, ) .expect("extra dependency did not compile"); module_types.insert(package.clone(), typed_module.type_info.clone()); @@ -50,6 +51,7 @@ fn check_module( &module_types, tracing, &mut warnings, + None, ); result diff --git a/crates/aiken-lang/src/tipo/environment.rs b/crates/aiken-lang/src/tipo/environment.rs index 81753bf4..2e8febf9 100644 --- a/crates/aiken-lang/src/tipo/environment.rs +++ b/crates/aiken-lang/src/tipo/environment.rs @@ -7,7 +7,7 @@ use super::{ }; use crate::{ ast::{ - Annotation, CallArg, DataType, Definition, Function, ModuleConstant, ModuleKind, + self, Annotation, CallArg, DataType, Definition, Function, ModuleConstant, ModuleKind, RecordConstructor, RecordConstructorArg, Span, TypeAlias, TypedDefinition, TypedFunction, TypedPattern, UnqualifiedImport, UntypedArg, UntypedDefinition, UntypedFunction, Use, Validator, PIPE_VARIABLE, @@ -80,11 +80,49 @@ pub struct Environment<'a> { /// A mapping from known annotations to their resolved type. pub annotations: HashMap>, + /// The user-defined target environment referred to as the module 'env'. + pub target_env: Option<&'a str>, + /// Warnings pub warnings: &'a mut Vec, } impl<'a> Environment<'a> { + pub fn find_module(&self, fragments: &[String], location: Span) -> Result<&'a TypeInfo, Error> { + let mut name = fragments.join("/"); + + let is_env = name == ast::ENV_MODULE; + + if is_env { + name = self + .target_env + .unwrap_or(ast::DEFAULT_ENV_MODULE) + .to_string() + } + + self.importable_modules.get(&name).ok_or_else(|| { + if is_env { + Error::UnknownEnvironment { + name, + known_environments: self + .importable_modules + .values() + .filter_map(|m| match m.kind { + ModuleKind::Env => Some(m.name.clone()), + ModuleKind::Lib | ModuleKind::Validator | ModuleKind::Config => None, + }) + .collect(), + } + } else { + Error::UnknownModule { + location, + name, + known_modules: self.importable_modules.keys().cloned().collect(), + } + } + }) + } + pub fn close_scope(&mut self, data: ScopeResetData) { let unused = self .entity_usages @@ -351,7 +389,7 @@ impl<'a> Environment<'a> { .ok_or_else(|| Error::UnknownModule { location, name: name.to_string(), - imported_modules: self + known_modules: self .importable_modules .keys() .map(|t| t.to_string()) @@ -397,7 +435,7 @@ impl<'a> Environment<'a> { .get(m) .ok_or_else(|| Error::UnknownModule { name: m.to_string(), - imported_modules: self + known_modules: self .importable_modules .keys() .map(|t| t.to_string()) @@ -705,6 +743,7 @@ impl<'a> Environment<'a> { current_kind: &'a ModuleKind, importable_modules: &'a HashMap, warnings: &'a mut Vec, + target_env: Option<&'a str>, ) -> Self { let prelude = importable_modules .get("aiken") @@ -731,6 +770,7 @@ impl<'a> Environment<'a> { annotations: HashMap::new(), warnings, entity_usages: vec![HashMap::new()], + target_env, } } @@ -772,24 +812,16 @@ impl<'a> Environment<'a> { location, package: _, }) => { - let name = module.join("/"); - - // Find imported module - let module_info = - self.importable_modules - .get(&name) - .ok_or_else(|| Error::UnknownModule { - location: *location, - name: name.clone(), - imported_modules: self.imported_modules.keys().cloned().collect(), - })?; + let module_info = self.find_module(module, *location)?; if module_info.kind.is_validator() - && (self.current_kind.is_lib() || !self.current_module.starts_with("tests")) + && (self.current_kind.is_lib() + || self.current_kind.is_env() + || !self.current_module.starts_with("tests")) { return Err(Error::ValidatorImported { location: *location, - name, + name: module.join("/"), }); } @@ -1710,7 +1742,7 @@ impl<'a> Environment<'a> { .ok_or_else(|| Error::UnknownModule { location, name: name.to_string(), - imported_modules: self + known_modules: self .importable_modules .keys() .map(|t| t.to_string()) diff --git a/crates/aiken-lang/src/tipo/error.rs b/crates/aiken-lang/src/tipo/error.rs index 6601d380..81ed0519 100644 --- a/crates/aiken-lang/src/tipo/error.rs +++ b/crates/aiken-lang/src/tipo/error.rs @@ -751,13 +751,39 @@ Perhaps, try the following: #[diagnostic(code("unknown::module"))] #[diagnostic(help( "{}", - suggest_neighbor(name, imported_modules.iter(), "Did you forget to add a package as dependency?") + suggest_neighbor(name, known_modules.iter(), "Did you forget to add a package as dependency?") ))] UnknownModule { #[label] location: Span, name: String, - imported_modules: Vec, + known_modules: Vec, + }, + + #[error( + "I couldn't find any module for the environment: '{}'\n", + name.if_supports_color(Stdout, |s| s.purple()) + )] + #[diagnostic(code("unknown::environment"))] + #[diagnostic(help( + "{}{}", + if known_environments.is_empty() { + String::new() + } else { + format!( + "I know about the following environments:\n{}\n\n", + known_environments + .iter() + .map(|s| format!("─▶ {}", s.if_supports_color(Stdout, |s| s.purple()))) + .collect::>() + .join("\n") + ) + }, + suggest_neighbor(name, known_environments.iter(), "Did you forget to define this environment?") + ))] + UnknownEnvironment { + name: String, + known_environments: Vec, }, #[error( @@ -1066,6 +1092,7 @@ impl ExtraData for Error { | Error::UnknownModuleType { .. } | Error::UnknownModuleValue { .. } | Error::UnknownRecordField { .. } + | Error::UnknownEnvironment { .. } | Error::UnnecessarySpreadOperator { .. } | Error::UpdateMultiConstructorType { .. } | Error::ValidatorImported { .. } diff --git a/crates/aiken-lang/src/tipo/expr.rs b/crates/aiken-lang/src/tipo/expr.rs index 9219de88..f6ceb1a1 100644 --- a/crates/aiken-lang/src/tipo/expr.rs +++ b/crates/aiken-lang/src/tipo/expr.rs @@ -956,9 +956,9 @@ impl<'a, 'b> ExprTyper<'a, 'b> { .ok_or_else(|| Error::UnknownModule { name: module_alias.to_string(), location: *module_location, - imported_modules: self + known_modules: self .environment - .imported_modules + .importable_modules .keys() .map(|t| t.to_string()) .collect(), @@ -2327,9 +2327,9 @@ impl<'a, 'b> ExprTyper<'a, 'b> { .ok_or_else(|| Error::UnknownModule { location: *location, name: module_name.to_string(), - imported_modules: self + known_modules: self .environment - .imported_modules + .importable_modules .keys() .map(|t| t.to_string()) .collect(), diff --git a/crates/aiken-lang/src/tipo/infer.rs b/crates/aiken-lang/src/tipo/infer.rs index b9a35b0e..8f9a37a6 100644 --- a/crates/aiken-lang/src/tipo/infer.rs +++ b/crates/aiken-lang/src/tipo/infer.rs @@ -19,6 +19,7 @@ use crate::{ use std::{borrow::Borrow, collections::HashMap, ops::Deref, rc::Rc}; impl UntypedModule { + #[allow(clippy::too_many_arguments)] pub fn infer( mut self, id_gen: &IdGenerator, @@ -27,11 +28,12 @@ impl UntypedModule { modules: &HashMap, tracing: Tracing, warnings: &mut Vec, + env: Option<&str>, ) -> Result { let module_name = self.name.clone(); let docs = std::mem::take(&mut self.docs); let mut environment = - Environment::new(id_gen.clone(), &module_name, &kind, modules, warnings); + Environment::new(id_gen.clone(), &module_name, &kind, modules, warnings, env); let mut type_names = HashMap::with_capacity(self.definitions.len()); let mut value_names = HashMap::with_capacity(self.definitions.len()); @@ -574,18 +576,7 @@ fn infer_definition( unqualified, package: _, }) => { - let name = module.join("/"); - - // Find imported module - let module_info = - environment - .importable_modules - .get(&name) - .ok_or_else(|| Error::UnknownModule { - location, - name, - imported_modules: environment.imported_modules.keys().cloned().collect(), - })?; + let module_info = environment.find_module(&module, location)?; Ok(Definition::Use(Use { location, diff --git a/crates/aiken-lsp/src/server/lsp_project.rs b/crates/aiken-lsp/src/server/lsp_project.rs index eda2d020..efd36ab6 100644 --- a/crates/aiken-lsp/src/server/lsp_project.rs +++ b/crates/aiken-lsp/src/server/lsp_project.rs @@ -40,6 +40,7 @@ impl LspProject { u32::default(), PropertyTest::DEFAULT_MAX_SUCCESS, Tracing::silent(), + None, ); self.project.restore(checkpoint); diff --git a/crates/aiken-project/src/config.rs b/crates/aiken-project/src/config.rs index d083bb0c..46464816 100644 --- a/crates/aiken-project/src/config.rs +++ b/crates/aiken-project/src/config.rs @@ -1,13 +1,20 @@ -use std::{fmt::Display, fs, io, path::Path}; - use crate::{github::repo::LatestRelease, package_name::PackageName, paths, Error}; -use aiken_lang::ast::Span; -use semver::Version; - -use miette::NamedSource; -use serde::{Deserialize, Serialize}; - pub use aiken_lang::plutus_version::PlutusVersion; +use aiken_lang::{ + ast::{ + Annotation, ByteArrayFormatPreference, Constant, ModuleConstant, Span, UntypedDefinition, + }, + expr::UntypedExpr, + parser::token::Base, +}; +use miette::NamedSource; +use semver::Version; +use serde::{ + de, + ser::{self, SerializeSeq, SerializeStruct}, + Deserialize, Serialize, +}; +use std::{collections::BTreeMap, fmt::Display, fs, io, path::Path}; #[derive(Deserialize, Serialize, Clone)] pub struct Config { @@ -27,6 +34,195 @@ pub struct Config { pub repository: Option, #[serde(default)] pub dependencies: Vec, + #[serde(default)] + pub config: BTreeMap>, +} + +#[derive(Clone, Debug)] +pub enum SimpleExpr { + Int(i64), + Bool(bool), + ByteArray(Vec, ByteArrayFormatPreference), + List(Vec), +} + +impl SimpleExpr { + pub fn as_untyped_expr(&self) -> UntypedExpr { + match self { + SimpleExpr::Bool(b) => UntypedExpr::Var { + location: Span::empty(), + name: if *b { "True" } else { "False" }.to_string(), + }, + SimpleExpr::Int(i) => UntypedExpr::UInt { + location: Span::empty(), + value: format!("{i}"), + base: Base::Decimal { + numeric_underscore: false, + }, + }, + SimpleExpr::ByteArray(bs, preferred_format) => UntypedExpr::ByteArray { + location: Span::empty(), + bytes: bs.to_vec(), + preferred_format: *preferred_format, + }, + SimpleExpr::List(es) => UntypedExpr::List { + location: Span::empty(), + elements: es.iter().map(|e| e.as_untyped_expr()).collect(), + tail: None, + }, + } + } + + pub fn as_definition(&self, identifier: &str) -> UntypedDefinition { + let location = Span::empty(); + + let (value, annotation) = match self { + SimpleExpr::Bool(..) => todo!("requires https://github.com/aiken-lang/aiken/pull/992"), + SimpleExpr::Int(i) => ( + // TODO: Replace with 'self.as_untyped_expr()' after https://github.com/aiken-lang/aiken/pull/992 + Constant::Int { + location, + value: format!("{i}"), + base: Base::Decimal { + numeric_underscore: false, + }, + }, + Some(Annotation::int(location)), + ), + SimpleExpr::ByteArray(bs, preferred_format) => ( + // TODO: Replace with 'self.as_untyped_expr()' after https://github.com/aiken-lang/aiken/pull/992 + Constant::ByteArray { + location, + bytes: bs.to_vec(), + preferred_format: *preferred_format, + }, + Some(Annotation::bytearray(location)), + ), + SimpleExpr::List(..) => todo!("requires https://github.com/aiken-lang/aiken/pull/992"), + }; + + UntypedDefinition::ModuleConstant(ModuleConstant { + location: Span::empty(), + doc: None, + public: true, + name: identifier.to_string(), + annotation, + value: Box::new(value), + tipo: (), + }) + } +} + +impl Serialize for SimpleExpr { + fn serialize(&self, serializer: S) -> Result { + match self { + SimpleExpr::Bool(b) => serializer.serialize_bool(*b), + SimpleExpr::Int(i) => serializer.serialize_i64(*i), + SimpleExpr::ByteArray(bs, preferred_format) => match preferred_format { + ByteArrayFormatPreference::Utf8String => { + serializer.serialize_str(String::from_utf8(bs.to_vec()).unwrap().as_str()) + } + ByteArrayFormatPreference::ArrayOfBytes(..) + | ByteArrayFormatPreference::HexadecimalString => { + let mut s = serializer.serialize_struct("ByteArray", 2)?; + s.serialize_field("bytes", &hex::encode(bs))?; + s.serialize_field("encoding", "base16")?; + s.end() + } + }, + SimpleExpr::List(es) => { + let mut seq = serializer.serialize_seq(Some(es.len()))?; + for e in es { + seq.serialize_element(e)?; + } + seq.end() + } + } + } +} + +impl<'a> Deserialize<'a> for SimpleExpr { + fn deserialize>(deserializer: D) -> Result { + struct SimpleExprVisitor; + + #[derive(Deserialize)] + enum Encoding { + #[serde(rename(deserialize = "utf8"))] + Utf8, + #[serde(rename(deserialize = "utf-8"))] + Utf8Bis, + #[serde(rename(deserialize = "hex"))] + Hex, + #[serde(rename(deserialize = "base16"))] + Base16, + } + + #[derive(Deserialize)] + struct Bytes { + bytes: String, + encoding: Encoding, + } + + impl<'a> de::Visitor<'a> for SimpleExprVisitor { + type Value = SimpleExpr; + + fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { + formatter.write_str("Int | Bool | ByteArray | List") + } + + fn visit_bool(self, b: bool) -> Result { + Ok(SimpleExpr::Bool(b)) + } + + fn visit_i64(self, i: i64) -> Result { + Ok(SimpleExpr::Int(i)) + } + + fn visit_str(self, s: &str) -> Result { + Ok(SimpleExpr::ByteArray( + s.as_bytes().to_vec(), + ByteArrayFormatPreference::Utf8String, + )) + } + + fn visit_map(self, map: V) -> Result + where + V: de::MapAccess<'a>, + { + let Bytes { bytes, encoding } = + Bytes::deserialize(de::value::MapAccessDeserializer::new(map))?; + + match encoding { + Encoding::Hex | Encoding::Base16 => match hex::decode(&bytes) { + Err(e) => Err(de::Error::custom(format!("invalid base16 string: {e:?}"))), + Ok(bytes) => Ok(SimpleExpr::ByteArray( + bytes, + ByteArrayFormatPreference::HexadecimalString, + )), + }, + Encoding::Utf8 | Encoding::Utf8Bis => Ok(SimpleExpr::ByteArray( + bytes.as_bytes().to_vec(), + ByteArrayFormatPreference::Utf8String, + )), + } + } + + fn visit_seq(self, mut seq: A) -> Result + where + A: de::SeqAccess<'a>, + { + let mut es = Vec::new(); + + while let Some(e) = seq.next_element()? { + es.push(e); + } + + Ok(SimpleExpr::List(es)) + } + } + + deserializer.deserialize_any(SimpleExprVisitor) + } } fn deserialize_version<'de, D>(deserializer: D) -> Result @@ -108,6 +304,7 @@ impl Config { }, source: Platform::Github, }], + config: BTreeMap::new(), } } @@ -181,3 +378,53 @@ Version: {}"#, compiler_version(true), ) } + +#[cfg(test)] +mod tests { + use super::*; + use proptest::prelude::*; + + #[allow(clippy::arc_with_non_send_sync)] + fn arbitrary_simple_expr() -> impl Strategy { + let leaf = prop_oneof![ + (any::)().prop_map(SimpleExpr::Int), + (any::)().prop_map(SimpleExpr::Bool), + "[a-z0-9]*".prop_map(|bytes| SimpleExpr::ByteArray( + bytes.as_bytes().to_vec(), + ByteArrayFormatPreference::Utf8String + )), + "([0-9a-f][0-9a-f])*".prop_map(|bytes| SimpleExpr::ByteArray( + bytes.as_bytes().to_vec(), + ByteArrayFormatPreference::HexadecimalString + )) + ]; + + leaf.prop_recursive(3, 8, 3, |inner| { + prop_oneof![ + inner.clone(), + prop::collection::vec(inner.clone(), 0..3).prop_map(SimpleExpr::List) + ] + }) + } + + #[derive(Deserialize, Serialize)] + struct TestConfig { + expr: SimpleExpr, + } + + proptest! { + #[test] + fn round_trip_simple_expr(expr in arbitrary_simple_expr()) { + let pretty = toml::to_string_pretty(&TestConfig { expr }); + assert!( + matches!( + pretty.as_ref().map(|s| toml::from_str::(s.as_str())), + Ok(Ok(..)), + ), + "\ncounterexample: {}\n", + pretty.unwrap_or_default(), + ) + + } + } +} diff --git a/crates/aiken-project/src/error.rs b/crates/aiken-project/src/error.rs index e6f890c7..c0bdee4f 100644 --- a/crates/aiken-project/src/error.rs +++ b/crates/aiken-project/src/error.rs @@ -127,6 +127,9 @@ pub enum Error { #[error("I couldn't find any exportable function named '{name}' in module '{module}'.")] ExportNotFound { module: String, name: String }, + + #[error("I located conditional modules under 'env', but no default one!")] + NoDefaultEnvironment, } impl Error { @@ -195,6 +198,7 @@ impl ExtraData for Error { | Error::NoValidatorNotFound { .. } | Error::MoreThanOneValidatorFound { .. } | Error::Module { .. } + | Error::NoDefaultEnvironment { .. } | Error::ExportNotFound { .. } => None, Error::Type { error, .. } => error.extra_data(), } @@ -224,6 +228,7 @@ impl GetSource for Error { | Error::NoValidatorNotFound { .. } | Error::MoreThanOneValidatorFound { .. } | Error::ExportNotFound { .. } + | Error::NoDefaultEnvironment { .. } | Error::Module { .. } => None, Error::DuplicateModule { second: path, .. } | Error::MissingManifest { path } @@ -252,6 +257,7 @@ impl GetSource for Error { | Error::Json { .. } | Error::MalformedStakeAddress { .. } | Error::NoValidatorNotFound { .. } + | Error::NoDefaultEnvironment { .. } | Error::MoreThanOneValidatorFound { .. } | Error::ExportNotFound { .. } | Error::Module { .. } => None, @@ -307,6 +313,7 @@ impl Diagnostic for Error { Error::NoValidatorNotFound { .. } => None, Error::MoreThanOneValidatorFound { .. } => None, Error::ExportNotFound { .. } => None, + Error::NoDefaultEnvironment { .. } => None, Error::Module(e) => e.code().map(boxed), } } @@ -330,6 +337,9 @@ impl Diagnostic for Error { Error::MissingManifest { .. } => Some(Box::new( "Try running `aiken new ` to initialise a project with an example manifest.", )), + Error::NoDefaultEnvironment { .. } => Some(Box::new( + "Environment module names are free, but there must be at least one named 'default.ak'.", + )), Error::TomlLoading { .. } => None, Error::Format { .. } => None, Error::TestFailure { .. } => None, @@ -408,6 +418,7 @@ impl Diagnostic for Error { Error::MalformedStakeAddress { .. } => None, Error::NoValidatorNotFound { .. } => None, Error::MoreThanOneValidatorFound { .. } => None, + Error::NoDefaultEnvironment { .. } => None, Error::Module(e) => e.labels(), } } @@ -419,6 +430,7 @@ impl Diagnostic for Error { Error::ImportCycle { .. } => None, Error::ExportNotFound { .. } => None, Error::Blueprint(e) => e.source_code(), + Error::NoDefaultEnvironment { .. } => None, Error::Parse { named, .. } => Some(named), Error::Type { named, .. } => Some(named), Error::StandardIo(_) => None, @@ -462,6 +474,7 @@ impl Diagnostic for Error { Error::MalformedStakeAddress { .. } => None, Error::NoValidatorNotFound { .. } => None, Error::MoreThanOneValidatorFound { .. } => None, + Error::NoDefaultEnvironment { .. } => None, Error::Module(e) => e.url(), } } @@ -476,6 +489,7 @@ impl Diagnostic for Error { Error::Parse { .. } => None, Error::Type { error, .. } => error.related(), Error::StandardIo(_) => None, + Error::NoDefaultEnvironment { .. } => None, Error::MissingManifest { .. } => None, Error::TomlLoading { .. } => None, Error::Format { .. } => None, @@ -512,6 +526,8 @@ pub enum Warning { InvalidModuleName { path: PathBuf }, #[error("aiken.toml demands compiler version {demanded}, but you are using {current}.")] CompilerVersionMismatch { demanded: String, current: String }, + #[error("No configuration found for environment {env}.")] + NoConfigurationForEnv { env: String }, } impl ExtraData for Warning { @@ -520,7 +536,8 @@ impl ExtraData for Warning { Warning::NoValidators { .. } | Warning::DependencyAlreadyExists { .. } | Warning::InvalidModuleName { .. } - | Warning::CompilerVersionMismatch { .. } => None, + | Warning::CompilerVersionMismatch { .. } + | Warning::NoConfigurationForEnv { .. } => None, Warning::Type { warning, .. } => warning.extra_data(), } } @@ -532,6 +549,7 @@ impl GetSource for Warning { Warning::InvalidModuleName { path } | Warning::Type { path, .. } => Some(path.clone()), Warning::NoValidators | Warning::DependencyAlreadyExists { .. } + | Warning::NoConfigurationForEnv { .. } | Warning::CompilerVersionMismatch { .. } => None, } } @@ -542,6 +560,7 @@ impl GetSource for Warning { Warning::NoValidators | Warning::InvalidModuleName { .. } | Warning::DependencyAlreadyExists { .. } + | Warning::NoConfigurationForEnv { .. } | Warning::CompilerVersionMismatch { .. } => None, } } @@ -557,6 +576,7 @@ impl Diagnostic for Warning { Warning::Type { named, .. } => Some(named), Warning::NoValidators | Warning::InvalidModuleName { .. } + | Warning::NoConfigurationForEnv { .. } | Warning::DependencyAlreadyExists { .. } | Warning::CompilerVersionMismatch { .. } => None, } @@ -568,6 +588,7 @@ impl Diagnostic for Warning { Warning::InvalidModuleName { .. } | Warning::NoValidators | Warning::DependencyAlreadyExists { .. } + | Warning::NoConfigurationForEnv { .. } | Warning::CompilerVersionMismatch { .. } => None, } } @@ -586,6 +607,9 @@ impl Diagnostic for Warning { Warning::DependencyAlreadyExists { .. } => { Some(Box::new("aiken::packages::already_exists")) } + Warning::NoConfigurationForEnv { .. } => { + Some(Box::new("aiken::project::config::missing::env")) + } } } @@ -603,6 +627,9 @@ impl Diagnostic for Warning { Warning::DependencyAlreadyExists { .. } => Some(Box::new( "If you need to change the version, try 'aiken packages upgrade' instead.", )), + Warning::NoConfigurationForEnv { .. } => Some(Box::new( + "When configuration keys are missing for a target environment, no 'config' module will be created. This may lead to issues down the line.", + )), } } } diff --git a/crates/aiken-project/src/lib.rs b/crates/aiken-project/src/lib.rs index f9200e6c..1ad009f3 100644 --- a/crates/aiken-project/src/lib.rs +++ b/crates/aiken-project/src/lib.rs @@ -32,11 +32,12 @@ use crate::{ }; use aiken_lang::{ ast::{ - DataTypeKey, Definition, FunctionAccessKey, ModuleKind, Tracing, TypedDataType, - TypedFunction, + self, DataTypeKey, Definition, FunctionAccessKey, ModuleKind, Tracing, TypedDataType, + TypedFunction, UntypedDefinition, }, builtins, expr::UntypedExpr, + format::{Formatter, DOCS_MAX_COLUMNS}, gen_uplc::CodeGenerator, line_numbers::LineNumbers, plutus_version::PlutusVersion, @@ -78,6 +79,12 @@ pub struct Checkpoint { defined_modules: HashMap, } +#[derive(Debug, Clone)] +enum AddModuleBy { + Source { name: String, code: String }, + Path(PathBuf), +} + pub struct Project where T: EventListener, @@ -184,10 +191,16 @@ where self.defined_modules = checkpoint.defined_modules; } - pub fn build(&mut self, uplc: bool, tracing: Tracing) -> Result<(), Vec> { + pub fn build( + &mut self, + uplc: bool, + tracing: Tracing, + env: Option, + ) -> Result<(), Vec> { let options = Options { code_gen_mode: CodeGenMode::Build(uplc), tracing, + env, }; self.compile(options) @@ -205,11 +218,13 @@ where version: self.config.version.clone(), }); - self.read_source_files()?; + let config = self.config_definitions(None); + + self.read_source_files(config)?; let mut modules = self.parse_sources(self.config.name.clone())?; - self.type_check(&mut modules, Tracing::silent(), false)?; + self.type_check(&mut modules, Tracing::silent(), None, false)?; let destination = destination.unwrap_or_else(|| self.root.join("docs")); @@ -250,9 +265,11 @@ where seed: u32, property_max_success: usize, tracing: Tracing, + env: Option, ) -> Result<(), Vec> { let options = Options { tracing, + env, code_gen_mode: if skip_tests { CodeGenMode::NoOp } else { @@ -293,6 +310,32 @@ where self.root.join("plutus.json") } + fn config_definitions(&mut self, env: Option<&str>) -> Option> { + if !self.config.config.is_empty() { + let env = env.unwrap_or(ast::DEFAULT_ENV_MODULE); + + match self.config.config.get(env) { + None => { + self.warnings.push(Warning::NoConfigurationForEnv { + env: env.to_string(), + }); + None + } + Some(config) => { + let mut conf_definitions = Vec::new(); + + for (identifier, value) in config.iter() { + conf_definitions.push(value.as_definition(identifier)); + } + + Some(conf_definitions) + } + } + } else { + None + } + } + pub fn compile(&mut self, options: Options) -> Result<(), Vec> { self.event_listener .handle_event(Event::StartingCompilation { @@ -301,11 +344,15 @@ where version: self.config.version.clone(), }); - self.read_source_files()?; + let env = options.env.as_deref(); + + let config = self.config_definitions(env); + + self.read_source_files(config)?; let mut modules = self.parse_sources(self.config.name.clone())?; - self.type_check(&mut modules, options.tracing, true)?; + self.type_check(&mut modules, options.tracing, env, true)?; match options.code_gen_mode { CodeGenMode::Build(uplc_dump) => { @@ -610,12 +657,28 @@ where Ok(()) } - fn read_source_files(&mut self) -> Result<(), Error> { + fn read_source_files(&mut self, config: Option>) -> Result<(), Error> { + let env = self.root.join("env"); let lib = self.root.join("lib"); let validators = self.root.join("validators"); + let root = self.root.clone(); + + if let Some(defs) = config { + self.add_module( + AddModuleBy::Source { + name: ast::CONFIG_MODULE.to_string(), + code: Formatter::new() + .definitions(&defs[..]) + .to_pretty_string(DOCS_MAX_COLUMNS), + }, + &root, + ModuleKind::Config, + )?; + } self.aiken_files(&validators, ModuleKind::Validator)?; self.aiken_files(&lib, ModuleKind::Lib)?; + self.aiken_files(&env, ModuleKind::Env)?; Ok(()) } @@ -721,6 +784,7 @@ where &mut self, modules: &mut ParsedModules, tracing: Tracing, + env: Option<&str>, validate_module_name: bool, ) -> Result<(), Vec> { let our_modules: BTreeSet = modules.keys().cloned().collect(); @@ -733,6 +797,7 @@ where &self.id_gen, &self.config.name.to_string(), tracing, + env, validate_module_name, &mut self.module_sources, &mut self.module_types, @@ -879,12 +944,18 @@ where } fn aiken_files(&mut self, dir: &Path, kind: ModuleKind) -> Result<(), Error> { + let mut has_default = None; + walkdir::WalkDir::new(dir) .follow_links(true) .into_iter() .filter_map(Result::ok) .filter(|e| e.file_type().is_file()) .try_for_each(|d| { + if has_default.is_none() { + has_default = Some(false); + } + let path = d.into_path(); let keep = is_aiken_path(&path, dir); let ext = path.extension(); @@ -895,19 +966,39 @@ where } if keep { - self.add_module(path, dir, kind) + if self.module_name(dir, &path).as_str() == ast::DEFAULT_ENV_MODULE { + has_default = Some(true); + } + self.add_module(AddModuleBy::Path(path), dir, kind) } else { Ok(()) } - }) + })?; + + if kind == ModuleKind::Env && has_default == Some(false) { + return Err(Error::NoDefaultEnvironment); + } + + Ok(()) } - fn add_module(&mut self, path: PathBuf, dir: &Path, kind: ModuleKind) -> Result<(), Error> { - let name = self.module_name(dir, &path); - let code = fs::read_to_string(&path).map_err(|error| Error::FileIo { - path: path.clone(), - error, - })?; + fn add_module( + &mut self, + add_by: AddModuleBy, + dir: &Path, + kind: ModuleKind, + ) -> Result<(), Error> { + let (name, code, path) = match add_by { + AddModuleBy::Path(path) => { + let name = self.module_name(dir, &path); + let code = fs::read_to_string(&path).map_err(|error| Error::FileIo { + path: path.clone(), + error, + })?; + (name, code, path) + } + AddModuleBy::Source { name, code } => (name, code, dir.to_path_buf()), + }; self.sources.push(Source { name, diff --git a/crates/aiken-project/src/module.rs b/crates/aiken-project/src/module.rs index 49aad2a5..f6a3b4e2 100644 --- a/crates/aiken-project/src/module.rs +++ b/crates/aiken-project/src/module.rs @@ -32,16 +32,9 @@ pub struct ParsedModule { } impl ParsedModule { - pub fn deps_for_graph(&self) -> (String, Vec) { + pub fn deps_for_graph(&self, env_modules: &[String]) -> (String, Vec) { let name = self.name.clone(); - - let deps: Vec<_> = self - .ast - .dependencies() - .into_iter() - .map(|(dep, _span)| dep) - .collect(); - + let deps: Vec<_> = self.ast.dependencies(env_modules); (name, deps) } @@ -51,6 +44,7 @@ impl ParsedModule { id_gen: &IdGenerator, package: &str, tracing: Tracing, + env: Option<&str>, validate_module_name: bool, module_sources: &mut HashMap, module_types: &mut HashMap, @@ -68,6 +62,7 @@ impl ParsedModule { module_types, tracing, &mut warnings, + env, ) .map_err(|error| Error::Type { path: self.path.clone(), @@ -122,10 +117,19 @@ impl ParsedModules { } pub fn sequence(&self, our_modules: &BTreeSet) -> Result, Error> { + let env_modules = self + .0 + .values() + .filter_map(|m| match m.kind { + ModuleKind::Env => Some(m.name.clone()), + ModuleKind::Lib | ModuleKind::Validator | ModuleKind::Config => None, + }) + .collect::>(); + let inputs = self .0 .values() - .map(|m| m.deps_for_graph()) + .map(|m| m.deps_for_graph(&env_modules)) .collect::)>>(); let capacity = inputs.len(); diff --git a/crates/aiken-project/src/options.rs b/crates/aiken-project/src/options.rs index aa0a8d33..c1551706 100644 --- a/crates/aiken-project/src/options.rs +++ b/crates/aiken-project/src/options.rs @@ -3,6 +3,7 @@ use aiken_lang::ast::Tracing; pub struct Options { pub code_gen_mode: CodeGenMode, pub tracing: Tracing, + pub env: Option, } impl Default for Options { @@ -10,6 +11,7 @@ impl Default for Options { Self { code_gen_mode: CodeGenMode::NoOp, tracing: Tracing::silent(), + env: None, } } } diff --git a/crates/aiken-project/src/test_framework.rs b/crates/aiken-project/src/test_framework.rs index 2ca9e5ba..60e005c4 100644 --- a/crates/aiken-project/src/test_framework.rs +++ b/crates/aiken-project/src/test_framework.rs @@ -1314,6 +1314,7 @@ mod test { &module_types, Tracing::All(TraceLevel::Verbose), &mut warnings, + None, ) .expect("Failed to type-check module."); diff --git a/crates/aiken-project/src/tests/mod.rs b/crates/aiken-project/src/tests/mod.rs index d0967542..6039c89c 100644 --- a/crates/aiken-project/src/tests/mod.rs +++ b/crates/aiken-project/src/tests/mod.rs @@ -99,6 +99,7 @@ impl TestProject { &self.module_types, Tracing::All(TraceLevel::Verbose), &mut warnings, + None, ) .expect("Failed to type-check module"); diff --git a/crates/aiken/src/cmd/blueprint/address.rs b/crates/aiken/src/cmd/blueprint/address.rs index 285b30d3..df54879e 100644 --- a/crates/aiken/src/cmd/blueprint/address.rs +++ b/crates/aiken/src/cmd/blueprint/address.rs @@ -1,4 +1,3 @@ -use aiken_lang::ast::Tracing; use aiken_project::watch::with_project; use std::path::PathBuf; @@ -20,10 +19,6 @@ pub struct Args { #[clap(long)] delegated_to: Option, - /// Force the project to be rebuilt, otherwise relies on existing artifacts (i.e. plutus.json) - #[clap(long)] - rebuild: bool, - /// Output the address for mainnet (this command defaults to testnet) #[clap(long)] mainnet: bool, @@ -35,15 +30,10 @@ pub fn exec( module, validator, delegated_to, - rebuild, mainnet, }: Args, ) -> miette::Result<()> { with_project(directory.as_deref(), false, |p| { - if rebuild { - p.build(false, Tracing::silent())?; - } - let title = module.as_ref().map(|m| { format!( "{m}{}", diff --git a/crates/aiken/src/cmd/blueprint/hash.rs b/crates/aiken/src/cmd/blueprint/hash.rs index ef67e702..c7370890 100644 --- a/crates/aiken/src/cmd/blueprint/hash.rs +++ b/crates/aiken/src/cmd/blueprint/hash.rs @@ -1,4 +1,3 @@ -use aiken_lang::ast::Tracing; use aiken_project::watch::with_project; use std::path::PathBuf; @@ -15,10 +14,6 @@ pub struct Args { /// Name of the validator within the module. Optional if there's only one validator #[clap(short, long)] validator: Option, - - /// Force the project to be rebuilt, otherwise relies on existing artifacts (i.e. plutus.json) - #[clap(long)] - rebuild: bool, } pub fn exec( @@ -26,14 +21,9 @@ pub fn exec( directory, module, validator, - rebuild, }: Args, ) -> miette::Result<()> { with_project(directory.as_deref(), false, |p| { - if rebuild { - p.build(false, Tracing::silent())?; - } - let title = module.as_ref().map(|m| { format!( "{m}{}", diff --git a/crates/aiken/src/cmd/blueprint/policy.rs b/crates/aiken/src/cmd/blueprint/policy.rs index 80c90d4c..e23f118c 100644 --- a/crates/aiken/src/cmd/blueprint/policy.rs +++ b/crates/aiken/src/cmd/blueprint/policy.rs @@ -1,4 +1,3 @@ -use aiken_lang::ast::Tracing; use aiken_project::watch::with_project; use std::path::PathBuf; @@ -15,10 +14,6 @@ pub struct Args { /// Name of the validator within the module. Optional if there's only one validator #[clap(short, long)] validator: Option, - - /// Force the project to be rebuilt, otherwise relies on existing artifacts (i.e. plutus.json) - #[clap(long)] - rebuild: bool, } pub fn exec( @@ -26,14 +21,9 @@ pub fn exec( directory, module, validator, - rebuild, }: Args, ) -> miette::Result<()> { with_project(directory.as_deref(), false, |p| { - if rebuild { - p.build(false, Tracing::silent())?; - } - let title = module.as_ref().map(|m| { format!( "{m}{}", diff --git a/crates/aiken/src/cmd/build.rs b/crates/aiken/src/cmd/build.rs index a5b60499..fbe89fba 100644 --- a/crates/aiken/src/cmd/build.rs +++ b/crates/aiken/src/cmd/build.rs @@ -21,6 +21,10 @@ pub struct Args { #[clap(short, long)] uplc: bool, + /// Environment to build against. + #[clap(long)] + env: Option, + /// Filter traces to be included in the generated program(s). /// /// - user-defined: @@ -63,6 +67,7 @@ pub fn exec( uplc, filter_traces, trace_level, + env, }: Args, ) -> miette::Result<()> { let result = if watch { @@ -73,6 +78,7 @@ pub fn exec( Some(filter_traces) => filter_traces(trace_level), None => Tracing::All(trace_level), }, + env.clone(), ) }) } else { @@ -83,6 +89,7 @@ pub fn exec( Some(filter_traces) => filter_traces(trace_level), None => Tracing::All(trace_level), }, + env.clone(), ) }) }; diff --git a/crates/aiken/src/cmd/check.rs b/crates/aiken/src/cmd/check.rs index 92f89d3a..d83c437f 100644 --- a/crates/aiken/src/cmd/check.rs +++ b/crates/aiken/src/cmd/check.rs @@ -48,6 +48,10 @@ pub struct Args { #[clap(short, long)] exact_match: bool, + /// Environment to build against. + #[clap(long)] + env: Option, + /// Filter traces to be included in the generated program(s). /// /// - user-defined: @@ -95,6 +99,7 @@ pub fn exec( trace_level, seed, max_success, + env, }: Args, ) -> miette::Result<()> { let mut rng = rand::thread_rng(); @@ -114,6 +119,7 @@ pub fn exec( Some(filter_traces) => filter_traces(trace_level), None => Tracing::All(trace_level), }, + env.clone(), ) }) } else { @@ -129,6 +135,7 @@ pub fn exec( Some(filter_traces) => filter_traces(trace_level), None => Tracing::All(trace_level), }, + env.clone(), ) }) };