From 6454266b06bd40b73cb163399febe542adab3c66 Mon Sep 17 00:00:00 2001 From: KtorZ Date: Sun, 4 Aug 2024 13:18:54 +0200 Subject: [PATCH] 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::>();