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.
This commit is contained in:
parent
6de1d91104
commit
6454266b06
|
@ -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<TypeInfo, TypedDefinition>;
|
||||
|
@ -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(),
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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(),
|
||||
}
|
||||
|
|
|
@ -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<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(String),
|
||||
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(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<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(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<D: de::Deserializer<'a>>(deserializer: D) -> Result<Self, D::Error> {
|
||||
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<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.to_string()))
|
||||
}
|
||||
|
||||
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 +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<Value = SimpleExpr> {
|
||||
let leaf = prop_oneof![
|
||||
(any::<i64>)().prop_map(SimpleExpr::Int),
|
||||
(any::<bool>)().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::<TestConfig>(s.as_str())),
|
||||
Ok(Ok(..)),
|
||||
),
|
||||
"\ncounterexample: {}\n",
|
||||
pretty.unwrap_or_default(),
|
||||
)
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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.",
|
||||
)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<String, PathBuf>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
enum AddModuleBy {
|
||||
Source { name: String, code: String },
|
||||
Path(PathBuf),
|
||||
}
|
||||
|
||||
pub struct Project<T>
|
||||
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<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 {
|
||||
|
@ -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<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)?;
|
||||
|
@ -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,
|
||||
|
|
|
@ -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::<Vec<String>>();
|
||||
|
||||
|
|
Loading…
Reference in New Issue