From c9d0da0c221c4b9aa1a6ab69c0bf79ed7eeeb2be Mon Sep 17 00:00:00 2001 From: KtorZ Date: Sat, 3 Aug 2024 17:42:55 +0200 Subject: [PATCH 1/7] Parse sources of conditional env modules. Do nothing about it yet, but trigger an error if env/default.ak is missing; but only if there's any module at all under env. --- crates/aiken-lang/src/ast.rs | 5 +++++ crates/aiken-lang/src/tipo/environment.rs | 4 +++- crates/aiken-project/src/error.rs | 14 ++++++++++++++ crates/aiken-project/src/lib.rs | 21 ++++++++++++++++++++- 4 files changed, 42 insertions(+), 2 deletions(-) diff --git a/crates/aiken-lang/src/ast.rs b/crates/aiken-lang/src/ast.rs index ac297986..885c2576 100644 --- a/crates/aiken-lang/src/ast.rs +++ b/crates/aiken-lang/src/ast.rs @@ -28,6 +28,7 @@ pub type UntypedModule = Module<(), UntypedDefinition>; pub enum ModuleKind { Lib, Validator, + Env, } impl ModuleKind { @@ -38,6 +39,10 @@ impl ModuleKind { pub fn is_lib(&self) -> bool { matches!(self, ModuleKind::Lib) } + + pub fn is_env(&self) -> bool { + matches!(self, ModuleKind::Env) + } } #[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)] diff --git a/crates/aiken-lang/src/tipo/environment.rs b/crates/aiken-lang/src/tipo/environment.rs index 81753bf4..564afa19 100644 --- a/crates/aiken-lang/src/tipo/environment.rs +++ b/crates/aiken-lang/src/tipo/environment.rs @@ -785,7 +785,9 @@ impl<'a> Environment<'a> { })?; 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, diff --git a/crates/aiken-project/src/error.rs b/crates/aiken-project/src/error.rs index e6f890c7..c544eb14 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, diff --git a/crates/aiken-project/src/lib.rs b/crates/aiken-project/src/lib.rs index f9200e6c..2af5f0eb 100644 --- a/crates/aiken-project/src/lib.rs +++ b/crates/aiken-project/src/lib.rs @@ -65,6 +65,8 @@ use uplc::{ PlutusData, }; +const DEFAULT_ENV_MODULE: &str = "default"; + #[derive(Debug)] pub struct Source { pub path: PathBuf, @@ -611,11 +613,13 @@ where } fn read_source_files(&mut self) -> Result<(), Error> { + let env = self.root.join("env"); let lib = self.root.join("lib"); let validators = self.root.join("validators"); self.aiken_files(&validators, ModuleKind::Validator)?; self.aiken_files(&lib, ModuleKind::Lib)?; + self.aiken_files(&env, ModuleKind::Env)?; Ok(()) } @@ -879,12 +883,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,11 +905,20 @@ where } if keep { + if self.module_name(dir, &path).as_str() == DEFAULT_ENV_MODULE { + has_default = Some(true); + } self.add_module(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> { From fbe2f8258282e20c135821410682c6caf73cf960 Mon Sep 17 00:00:00 2001 From: KtorZ Date: Sun, 4 Aug 2024 10:27:32 +0200 Subject: [PATCH 2/7] Thread down environment module from cli down to the type-checker We simply provide a flag with a free-form output which acts as the module to lookup in the 'env' folder. The strategy is to replace the environment module name on-the-fly when a user tries to import 'env'. If the environment isn't found, an 'UnknownModule' error is raised (which I will slightly adjust in a following commits to something more related to environment) There are few important consequences to this design which may not seem immediately obvious: 1. We parse and type-check every env modules, even if they aren't used. This ensures that code doesn't break with a compilation error simply because people forgot to type-check a given env. Note that compilation could still fail because the env module itself could provide an invalid API. So it only prevents each modules to be independently wrong when taken in isolation. 2. Technically, this also means that one can import env modules in other env modules by their names. I don't know if it's a good or bad idea at this point but it doesn't really do any wrong; dependencies and cycles are handlded all-the-same. --- crates/aiken-lang/src/ast.rs | 3 ++ crates/aiken-lang/src/lib.rs | 1 + crates/aiken-lang/src/tests/check.rs | 2 ++ crates/aiken-lang/src/tipo/environment.rs | 40 +++++++++++++++------- crates/aiken-lang/src/tipo/infer.rs | 17 +++------ crates/aiken-lsp/src/server/lsp_project.rs | 1 + crates/aiken-project/src/lib.rs | 22 ++++++++---- crates/aiken-project/src/module.rs | 2 ++ crates/aiken-project/src/options.rs | 2 ++ crates/aiken-project/src/test_framework.rs | 1 + crates/aiken-project/src/tests/mod.rs | 1 + crates/aiken/src/cmd/blueprint/address.rs | 10 ------ crates/aiken/src/cmd/blueprint/hash.rs | 10 ------ crates/aiken/src/cmd/blueprint/policy.rs | 10 ------ crates/aiken/src/cmd/build.rs | 7 ++++ crates/aiken/src/cmd/check.rs | 7 ++++ 16 files changed, 73 insertions(+), 63 deletions(-) diff --git a/crates/aiken-lang/src/ast.rs b/crates/aiken-lang/src/ast.rs index 885c2576..dae941c2 100644 --- a/crates/aiken-lang/src/ast.rs +++ b/crates/aiken-lang/src/ast.rs @@ -21,6 +21,9 @@ 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 DEFAULT_ENV_MODULE: &str = "default"; + pub type TypedModule = Module; pub type UntypedModule = Module<(), UntypedDefinition>; 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 564afa19..4548bc7a 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,33 @@ 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("/"); + + if name == ast::ENV_MODULE { + name = self + .target_env + .unwrap_or(ast::DEFAULT_ENV_MODULE) + .to_string() + } + + self.importable_modules + .get(&name) + .ok_or_else(|| Error::UnknownModule { + location, + name, + imported_modules: self.imported_modules.keys().cloned().collect(), + }) + } + pub fn close_scope(&mut self, data: ScopeResetData) { let unused = self .entity_usages @@ -705,6 +727,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 +754,7 @@ impl<'a> Environment<'a> { annotations: HashMap::new(), warnings, entity_usages: vec![HashMap::new()], + target_env, } } @@ -772,17 +796,7 @@ 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() @@ -791,7 +805,7 @@ impl<'a> Environment<'a> { { return Err(Error::ValidatorImported { location: *location, - name, + name: module.join("/"), }); } 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/lib.rs b/crates/aiken-project/src/lib.rs index 2af5f0eb..e3e7607c 100644 --- a/crates/aiken-project/src/lib.rs +++ b/crates/aiken-project/src/lib.rs @@ -32,7 +32,7 @@ use crate::{ }; use aiken_lang::{ ast::{ - DataTypeKey, Definition, FunctionAccessKey, ModuleKind, Tracing, TypedDataType, + self, DataTypeKey, Definition, FunctionAccessKey, ModuleKind, Tracing, TypedDataType, TypedFunction, }, builtins, @@ -65,8 +65,6 @@ use uplc::{ PlutusData, }; -const DEFAULT_ENV_MODULE: &str = "default"; - #[derive(Debug)] pub struct Source { pub path: PathBuf, @@ -186,10 +184,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) @@ -211,7 +215,7 @@ where 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")); @@ -252,9 +256,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 { @@ -307,7 +313,7 @@ where 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, options.env.as_deref(), true)?; match options.code_gen_mode { CodeGenMode::Build(uplc_dump) => { @@ -725,6 +731,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(); @@ -737,6 +744,7 @@ where &self.id_gen, &self.config.name.to_string(), tracing, + env, validate_module_name, &mut self.module_sources, &mut self.module_types, @@ -905,7 +913,7 @@ where } if keep { - if self.module_name(dir, &path).as_str() == DEFAULT_ENV_MODULE { + if self.module_name(dir, &path).as_str() == ast::DEFAULT_ENV_MODULE { has_default = Some(true); } self.add_module(path, dir, kind) diff --git a/crates/aiken-project/src/module.rs b/crates/aiken-project/src/module.rs index 49aad2a5..804d5c6b 100644 --- a/crates/aiken-project/src/module.rs +++ b/crates/aiken-project/src/module.rs @@ -51,6 +51,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 +69,7 @@ impl ParsedModule { module_types, tracing, &mut warnings, + env, ) .map_err(|error| Error::Type { path: self.path.clone(), 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(), ) }) }; From 2dca0c418565fd461482822671eec165718cd6dd Mon Sep 17 00:00:00 2001 From: KtorZ Date: Sun, 4 Aug 2024 10:33:58 +0200 Subject: [PATCH 3/7] Ensure env modules dependencies are properly handled. We figure out dependencies by looking at 'use' definition in parsed modules. However, in the case of environment modules, we must consider all of them when seeing "use env". Without that, the env modules are simply compiled in parallel and may not yet have been compiled when they are needed as actual dependencies. --- crates/aiken-lang/src/ast.rs | 16 +++++++++------- crates/aiken-project/src/module.rs | 22 ++++++++++++---------- 2 files changed, 21 insertions(+), 17 deletions(-) diff --git a/crates/aiken-lang/src/ast.rs b/crates/aiken-lang/src/ast.rs index dae941c2..f9966940 100644 --- a/crates/aiken-lang/src/ast.rs +++ b/crates/aiken-lang/src/ast.rs @@ -69,16 +69,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() diff --git a/crates/aiken-project/src/module.rs b/crates/aiken-project/src/module.rs index 804d5c6b..35ba60b6 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) } @@ -124,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 => 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(); From 6de1d91104894b9083a001046687fed7aa7a05d1 Mon Sep 17 00:00:00 2001 From: KtorZ Date: Sun, 4 Aug 2024 11:02:15 +0200 Subject: [PATCH 4/7] Create dedicated error when environment isn't found. This is less confusing that getting an 'UnknownModule' error reporting even a different module name than the one actually being important ('env'). Also, this commit fixes a few errors found in the type-checker when reporting 'UnknownModule' errors. About half the time, we would actually attached _imported modules_ instead of _importable modules_ to the error, making the neighboring suggestion quite worse (nay useless). --- crates/aiken-lang/src/tipo/environment.rs | 38 ++++++++++++++++------- crates/aiken-lang/src/tipo/error.rs | 31 ++++++++++++++++-- crates/aiken-lang/src/tipo/expr.rs | 8 ++--- 3 files changed, 60 insertions(+), 17 deletions(-) diff --git a/crates/aiken-lang/src/tipo/environment.rs b/crates/aiken-lang/src/tipo/environment.rs index 4548bc7a..96805465 100644 --- a/crates/aiken-lang/src/tipo/environment.rs +++ b/crates/aiken-lang/src/tipo/environment.rs @@ -91,20 +91,36 @@ impl<'a> Environment<'a> { pub fn find_module(&self, fragments: &[String], location: Span) -> Result<&'a TypeInfo, Error> { let mut name = fragments.join("/"); - if name == ast::ENV_MODULE { + 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(|| Error::UnknownModule { - location, - name, - imported_modules: self.imported_modules.keys().cloned().collect(), - }) + 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 => None, + }) + .collect(), + } + } else { + Error::UnknownModule { + location, + name, + known_modules: self.importable_modules.keys().cloned().collect(), + } + } + }) } pub fn close_scope(&mut self, data: ScopeResetData) { @@ -373,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()) @@ -419,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()) @@ -1726,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(), From 6454266b06bd40b73cb163399febe542adab3c66 Mon Sep 17 00:00:00 2001 From: KtorZ Date: Sun, 4 Aug 2024 13:18:54 +0200 Subject: [PATCH 5/7] Allow simple expressions as configuration in aiken.toml This is currently extremely limited as it only supports (UTF-8) bytearrays and integers. We should seek to at least support hex bytes sequences, as well as bools, lists and possibly options. For the latter, we the rework on constant outlined in #992 is necessary. --- crates/aiken-lang/src/ast.rs | 15 ++ crates/aiken-lang/src/format.rs | 6 +- crates/aiken-lang/src/tipo/environment.rs | 2 +- crates/aiken-project/src/config.rs | 202 +++++++++++++++++++++- crates/aiken-project/src/error.rs | 15 +- crates/aiken-project/src/lib.rs | 88 ++++++++-- crates/aiken-project/src/module.rs | 2 +- 7 files changed, 304 insertions(+), 26 deletions(-) diff --git a/crates/aiken-lang/src/ast.rs b/crates/aiken-lang/src/ast.rs index f9966940..3f462192 100644 --- a/crates/aiken-lang/src/ast.rs +++ b/crates/aiken-lang/src/ast.rs @@ -22,6 +22,7 @@ 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; @@ -32,6 +33,7 @@ pub enum ModuleKind { Lib, Validator, Env, + Config, } impl ModuleKind { @@ -46,6 +48,10 @@ impl ModuleKind { 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)] @@ -1079,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/tipo/environment.rs b/crates/aiken-lang/src/tipo/environment.rs index 96805465..2e8febf9 100644 --- a/crates/aiken-lang/src/tipo/environment.rs +++ b/crates/aiken-lang/src/tipo/environment.rs @@ -109,7 +109,7 @@ impl<'a> Environment<'a> { .values() .filter_map(|m| match m.kind { ModuleKind::Env => Some(m.name.clone()), - ModuleKind::Lib | ModuleKind::Validator => None, + ModuleKind::Lib | ModuleKind::Validator | ModuleKind::Config => None, }) .collect(), } diff --git a/crates/aiken-project/src/config.rs b/crates/aiken-project/src/config.rs index d083bb0c..90993bb2 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}, + Deserialize, Serialize, +}; +use std::{collections::BTreeMap, fmt::Display, fs, io, path::Path}; #[derive(Deserialize, Serialize, Clone)] pub struct Config { @@ -27,6 +34,141 @@ 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(String), + 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(s) => UntypedExpr::ByteArray { + location: Span::empty(), + bytes: s.as_bytes().to_vec(), + preferred_format: ByteArrayFormatPreference::Utf8String, + }, + 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(s) => ( + // TODO: Replace with 'self.as_untyped_expr()' after https://github.com/aiken-lang/aiken/pull/992 + Constant::ByteArray { + location, + bytes: s.as_bytes().to_vec(), + preferred_format: ByteArrayFormatPreference::Utf8String, + }, + 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(s) => serializer.serialize_str(s.as_str()), + 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; + + 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.to_string())) + } + + 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 +250,7 @@ impl Config { }, source: Platform::Github, }], + config: BTreeMap::new(), } } @@ -181,3 +324,46 @@ 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-z]*".prop_map(SimpleExpr::ByteArray) + ]; + + 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 c544eb14..c0bdee4f 100644 --- a/crates/aiken-project/src/error.rs +++ b/crates/aiken-project/src/error.rs @@ -526,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 { @@ -534,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(), } } @@ -546,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, } } @@ -556,6 +560,7 @@ impl GetSource for Warning { Warning::NoValidators | Warning::InvalidModuleName { .. } | Warning::DependencyAlreadyExists { .. } + | Warning::NoConfigurationForEnv { .. } | Warning::CompilerVersionMismatch { .. } => None, } } @@ -571,6 +576,7 @@ impl Diagnostic for Warning { Warning::Type { named, .. } => Some(named), Warning::NoValidators | Warning::InvalidModuleName { .. } + | Warning::NoConfigurationForEnv { .. } | Warning::DependencyAlreadyExists { .. } | Warning::CompilerVersionMismatch { .. } => None, } @@ -582,6 +588,7 @@ impl Diagnostic for Warning { Warning::InvalidModuleName { .. } | Warning::NoValidators | Warning::DependencyAlreadyExists { .. } + | Warning::NoConfigurationForEnv { .. } | Warning::CompilerVersionMismatch { .. } => None, } } @@ -600,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")) + } } } @@ -617,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 e3e7607c..1ad009f3 100644 --- a/crates/aiken-project/src/lib.rs +++ b/crates/aiken-project/src/lib.rs @@ -33,10 +33,11 @@ use crate::{ use aiken_lang::{ ast::{ self, DataTypeKey, Definition, FunctionAccessKey, ModuleKind, Tracing, TypedDataType, - TypedFunction, + 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, @@ -211,7 +218,9 @@ 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())?; @@ -301,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 { @@ -309,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, options.env.as_deref(), true)?; + self.type_check(&mut modules, options.tracing, env, true)?; match options.code_gen_mode { CodeGenMode::Build(uplc_dump) => { @@ -618,10 +657,24 @@ 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)?; @@ -916,7 +969,7 @@ where if self.module_name(dir, &path).as_str() == ast::DEFAULT_ENV_MODULE { has_default = Some(true); } - self.add_module(path, dir, kind) + self.add_module(AddModuleBy::Path(path), dir, kind) } else { Ok(()) } @@ -929,12 +982,23 @@ where 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 35ba60b6..f6a3b4e2 100644 --- a/crates/aiken-project/src/module.rs +++ b/crates/aiken-project/src/module.rs @@ -122,7 +122,7 @@ impl ParsedModules { .values() .filter_map(|m| match m.kind { ModuleKind::Env => Some(m.name.clone()), - ModuleKind::Lib | ModuleKind::Validator => None, + ModuleKind::Lib | ModuleKind::Validator | ModuleKind::Config => None, }) .collect::>(); From ec7f659539315ddcdd338b760dc3122d70921054 Mon Sep 17 00:00:00 2001 From: KtorZ Date: Sun, 4 Aug 2024 14:29:05 +0200 Subject: [PATCH 6/7] Allow bytes to be defined as plain strings, or with specified encoding. The syntax is as follows: { "bytes" = "...", "encoding" = "" } The following encoding are accepted: "utf8", "utf-8", "hex", "base16" Note: the duplicates are only there to make it easier for people to discover them by accident. When "hex" (resp. "base16") is specified, the bytes string will be decoded and must be a valid hex string. --- crates/aiken-project/src/config.rs | 83 ++++++++++++++++++++++++++---- 1 file changed, 72 insertions(+), 11 deletions(-) diff --git a/crates/aiken-project/src/config.rs b/crates/aiken-project/src/config.rs index 90993bb2..46464816 100644 --- a/crates/aiken-project/src/config.rs +++ b/crates/aiken-project/src/config.rs @@ -11,7 +11,7 @@ use miette::NamedSource; use semver::Version; use serde::{ de, - ser::{self, SerializeSeq}, + ser::{self, SerializeSeq, SerializeStruct}, Deserialize, Serialize, }; use std::{collections::BTreeMap, fmt::Display, fs, io, path::Path}; @@ -42,7 +42,7 @@ pub struct Config { pub enum SimpleExpr { Int(i64), Bool(bool), - ByteArray(String), + ByteArray(Vec, ByteArrayFormatPreference), List(Vec), } @@ -60,10 +60,10 @@ impl SimpleExpr { numeric_underscore: false, }, }, - SimpleExpr::ByteArray(s) => UntypedExpr::ByteArray { + SimpleExpr::ByteArray(bs, preferred_format) => UntypedExpr::ByteArray { location: Span::empty(), - bytes: s.as_bytes().to_vec(), - preferred_format: ByteArrayFormatPreference::Utf8String, + bytes: bs.to_vec(), + preferred_format: *preferred_format, }, SimpleExpr::List(es) => UntypedExpr::List { location: Span::empty(), @@ -89,12 +89,12 @@ impl SimpleExpr { }, Some(Annotation::int(location)), ), - SimpleExpr::ByteArray(s) => ( + 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: s.as_bytes().to_vec(), - preferred_format: ByteArrayFormatPreference::Utf8String, + bytes: bs.to_vec(), + preferred_format: *preferred_format, }, Some(Annotation::bytearray(location)), ), @@ -118,7 +118,18 @@ impl Serialize for SimpleExpr { match self { SimpleExpr::Bool(b) => serializer.serialize_bool(*b), SimpleExpr::Int(i) => serializer.serialize_i64(*i), - SimpleExpr::ByteArray(s) => serializer.serialize_str(s.as_str()), + 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 { @@ -134,6 +145,24 @@ 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; @@ -150,7 +179,32 @@ impl<'a> Deserialize<'a> for SimpleExpr { } fn visit_str(self, s: &str) -> Result { - Ok(SimpleExpr::ByteArray(s.to_string())) + 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 @@ -335,7 +389,14 @@ mod tests { let leaf = prop_oneof![ (any::)().prop_map(SimpleExpr::Int), (any::)().prop_map(SimpleExpr::Bool), - "[a-z]*".prop_map(SimpleExpr::ByteArray) + "[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| { From a07f8cbc5868d6e20384c7c4dc2a613b72671988 Mon Sep 17 00:00:00 2001 From: KtorZ Date: Sun, 4 Aug 2024 14:33:15 +0200 Subject: [PATCH 7/7] Fill-in CHANGELOG. --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) 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