Merge pull request #994 from aiken-lang/conditional-modules

Conditional configuration & environment
This commit is contained in:
Matthias Benkort 2024-08-06 09:05:11 +02:00 committed by GitHub
commit e6bb13def6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
22 changed files with 548 additions and 111 deletions

View File

@ -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

View File

@ -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<TypeInfo, TypedDefinition>;
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<Info, Definitions> Module<Info, Definitions> {
}
impl UntypedModule {
pub fn dependencies(&self) -> Vec<(String, Span)> {
pub fn dependencies(&self, env_modules: &[String]) -> Vec<String> {
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(),

View File

@ -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();

View File

@ -49,6 +49,7 @@ macro_rules! aiken_fn {
$module_types,
$crate::ast::Tracing::silent(),
&mut warnings,
None,
)
.unwrap();

View File

@ -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

View File

@ -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<Annotation, Rc<Type>>,
/// The user-defined target environment referred to as the module 'env'.
pub target_env: Option<&'a str>,
/// Warnings
pub warnings: &'a mut Vec<Warning>,
}
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<String, TypeInfo>,
warnings: &'a mut Vec<Warning>,
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())

View File

@ -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<String>,
known_modules: Vec<String>,
},
#[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::<Vec<_>>()
.join("\n")
)
},
suggest_neighbor(name, known_environments.iter(), "Did you forget to define this environment?")
))]
UnknownEnvironment {
name: String,
known_environments: Vec<String>,
},
#[error(
@ -1066,6 +1092,7 @@ impl ExtraData for Error {
| Error::UnknownModuleType { .. }
| Error::UnknownModuleValue { .. }
| Error::UnknownRecordField { .. }
| Error::UnknownEnvironment { .. }
| Error::UnnecessarySpreadOperator { .. }
| Error::UpdateMultiConstructorType { .. }
| Error::ValidatorImported { .. }

View File

@ -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(),

View File

@ -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<String, TypeInfo>,
tracing: Tracing,
warnings: &mut Vec<Warning>,
env: Option<&str>,
) -> Result<TypedModule, Error> {
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,

View File

@ -40,6 +40,7 @@ impl LspProject {
u32::default(),
PropertyTest::DEFAULT_MAX_SUCCESS,
Tracing::silent(),
None,
);
self.project.restore(checkpoint);

View File

@ -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<Repository>,
#[serde(default)]
pub dependencies: Vec<Dependency>,
#[serde(default)]
pub config: BTreeMap<String, BTreeMap<String, SimpleExpr>>,
}
#[derive(Clone, Debug)]
pub enum SimpleExpr {
Int(i64),
Bool(bool),
ByteArray(Vec<u8>, ByteArrayFormatPreference),
List(Vec<SimpleExpr>),
}
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<S: ser::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
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<D: de::Deserializer<'a>>(deserializer: D) -> Result<Self, D::Error> {
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<any_of_those>")
}
fn visit_bool<E>(self, b: bool) -> Result<Self::Value, E> {
Ok(SimpleExpr::Bool(b))
}
fn visit_i64<E>(self, i: i64) -> Result<Self::Value, E> {
Ok(SimpleExpr::Int(i))
}
fn visit_str<E>(self, s: &str) -> Result<Self::Value, E> {
Ok(SimpleExpr::ByteArray(
s.as_bytes().to_vec(),
ByteArrayFormatPreference::Utf8String,
))
}
fn visit_map<V>(self, map: V) -> Result<Self::Value, V::Error>
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<A>(self, mut seq: A) -> Result<Self::Value, A::Error>
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<Version, D::Error>
@ -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<Value = SimpleExpr> {
let leaf = prop_oneof![
(any::<i64>)().prop_map(SimpleExpr::Int),
(any::<bool>)().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::<TestConfig>(s.as_str())),
Ok(Ok(..)),
),
"\ncounterexample: {}\n",
pretty.unwrap_or_default(),
)
}
}
}

View File

@ -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 <REPOSITORY/PROJECT>` 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.",
)),
}
}
}

View File

@ -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<String, PathBuf>,
}
#[derive(Debug, Clone)]
enum AddModuleBy {
Source { name: String, code: String },
Path(PathBuf),
}
pub struct Project<T>
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<Error>> {
pub fn build(
&mut self,
uplc: bool,
tracing: Tracing,
env: Option<String>,
) -> Result<(), Vec<Error>> {
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<String>,
) -> Result<(), Vec<Error>> {
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<Vec<UntypedDefinition>> {
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<Error>> {
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<Vec<UntypedDefinition>>) -> 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<Error>> {
let our_modules: BTreeSet<String> = 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,

View File

@ -32,16 +32,9 @@ pub struct ParsedModule {
}
impl ParsedModule {
pub fn deps_for_graph(&self) -> (String, Vec<String>) {
pub fn deps_for_graph(&self, env_modules: &[String]) -> (String, Vec<String>) {
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<String, (String, LineNumbers)>,
module_types: &mut HashMap<String, TypeInfo>,
@ -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<String>) -> Result<Vec<String>, 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::<Vec<String>>();
let inputs = self
.0
.values()
.map(|m| m.deps_for_graph())
.map(|m| m.deps_for_graph(&env_modules))
.collect::<Vec<(String, Vec<String>)>>();
let capacity = inputs.len();

View File

@ -3,6 +3,7 @@ use aiken_lang::ast::Tracing;
pub struct Options {
pub code_gen_mode: CodeGenMode,
pub tracing: Tracing,
pub env: Option<String>,
}
impl Default for Options {
@ -10,6 +11,7 @@ impl Default for Options {
Self {
code_gen_mode: CodeGenMode::NoOp,
tracing: Tracing::silent(),
env: None,
}
}
}

View File

@ -1314,6 +1314,7 @@ mod test {
&module_types,
Tracing::All(TraceLevel::Verbose),
&mut warnings,
None,
)
.expect("Failed to type-check module.");

View File

@ -99,6 +99,7 @@ impl TestProject {
&self.module_types,
Tracing::All(TraceLevel::Verbose),
&mut warnings,
None,
)
.expect("Failed to type-check module");

View File

@ -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<String>,
/// 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}{}",

View File

@ -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<String>,
/// 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}{}",

View File

@ -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<String>,
/// 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}{}",

View File

@ -21,6 +21,10 @@ pub struct Args {
#[clap(short, long)]
uplc: bool,
/// Environment to build against.
#[clap(long)]
env: Option<String>,
/// 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(),
)
})
};

View File

@ -48,6 +48,10 @@ pub struct Args {
#[clap(short, long)]
exact_match: bool,
/// Environment to build against.
#[clap(long)]
env: Option<String>,
/// 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(),
)
})
};