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:
KtorZ 2024-08-04 13:18:54 +02:00
parent 6de1d91104
commit 6454266b06
No known key found for this signature in database
GPG Key ID: 33173CB6F77F4277
7 changed files with 304 additions and 26 deletions

View File

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

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

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

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

View File

@ -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.",
)),
}
}
}

View File

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

View File

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