diff --git a/CHANGELOG.md b/CHANGELOG.md index e2b6ff8a..5815f562 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,12 @@ - **aiken**: new command `blueprint convert` +### Changed + +- **aiken-lang**: block `Data` and `String` from unifying when casting +- **aiken-lang**: remove ability for a type with many variants with matching field labels and types to support field access +- **aiken-project**: tests filtering with `-m` during check now happens in `Project::collect_tests` + ## [v0.0.29] - 2023-MM-DD ### Added diff --git a/crates/aiken-lang/src/air.rs b/crates/aiken-lang/src/air.rs index 1c063406..42808933 100644 --- a/crates/aiken-lang/src/air.rs +++ b/crates/aiken-lang/src/air.rs @@ -117,6 +117,11 @@ pub enum Air { constr_index: usize, }, + AssertBool { + scope: Vec, + is_true: bool, + }, + // When When { scope: Vec, @@ -268,6 +273,7 @@ impl Air { | Air::UnWrapData { scope, .. } | Air::WrapData { scope, .. } | Air::AssertConstr { scope, .. } + | Air::AssertBool { scope, .. } | Air::When { scope, .. } | Air::Clause { scope, .. } | Air::ListClause { scope, .. } @@ -375,6 +381,7 @@ impl Air { | Air::Let { .. } | Air::WrapClause { .. } | Air::AssertConstr { .. } + | Air::AssertBool { .. } | Air::Finally { .. } | Air::FieldsExpose { .. } => None, diff --git a/crates/aiken-lang/src/builder.rs b/crates/aiken-lang/src/builder.rs index 2a4f170e..bcee8f50 100644 --- a/crates/aiken-lang/src/builder.rs +++ b/crates/aiken-lang/src/builder.rs @@ -1200,7 +1200,7 @@ pub fn find_and_replace_generics(tipo: &mut Arc, mono_types: &IndexMap Vec<(u64, Arc)> { +pub fn get_generic_id_and_type(tipo: &Type, param: &Type) -> Vec<(u64, Arc)> { let mut generics_ids = vec![]; if let Some(id) = tipo.get_generic() { @@ -1213,7 +1213,7 @@ pub fn get_generics_and_type(tipo: &Type, param: &Type) -> Vec<(u64, Arc)> .iter() .zip(param.get_inner_types().iter()) { - generics_ids.append(&mut get_generics_and_type(tipo, param_type)); + generics_ids.append(&mut get_generic_id_and_type(tipo, param_type)); } generics_ids } @@ -1990,7 +1990,7 @@ pub fn replace_opaque_type(t: &mut Arc, data_types: IndexMap Environment<'a> { }, ); } + Ok(Some(fields)) } @@ -1289,6 +1290,7 @@ impl<'a> Environment<'a> { && !(t1.is_unbound() || t2.is_unbound()) && !(t1.is_function() || t2.is_function()) && !(t1.is_generic() || t2.is_generic()) + && !(t1.is_string() || t2.is_string()) { return Ok(()); } @@ -1779,44 +1781,22 @@ fn get_compatible_record_fields( ) -> Vec<(usize, &str, &Annotation)> { let mut compatible = vec![]; + if constructors.len() > 1 { + return compatible; + } + let first = match constructors.get(0) { Some(first) => first, None => return compatible, }; - 'next_argument: for (index, first_argument) in first.arguments.iter().enumerate() { + for (index, first_argument) in first.arguments.iter().enumerate() { // Fields without labels do not have accessors let label = match first_argument.label.as_ref() { Some(label) => label.as_str(), - None => continue 'next_argument, + None => continue, }; - // Check each variant to see if they have an field in the same position - // with the same label and the same type - for constructor in constructors.iter().skip(1) { - // The field must exist in all variants - let argument = match constructor.arguments.get(index) { - Some(argument) => argument, - None => continue 'next_argument, - }; - - // The labels must be the same - if argument.label != first_argument.label { - continue 'next_argument; - } - - // The types must be the same - if !argument - .annotation - .is_logically_equal(&first_argument.annotation) - { - continue 'next_argument; - } - } - - // The previous loop did not find any incompatible fields in the other - // variants so this field is compatible across variants and we should - // generate an accessor for it. compatible.push((index, label, &first_argument.annotation)) } diff --git a/crates/aiken-lang/src/tipo/error.rs b/crates/aiken-lang/src/tipo/error.rs index 6d6d418f..6aa80597 100644 --- a/crates/aiken-lang/src/tipo/error.rs +++ b/crates/aiken-lang/src/tipo/error.rs @@ -264,8 +264,9 @@ You can use '{discard}' and numbers to distinguish between similar names. }, #[error( - "I saw a {} fields in a context where there should be {}.\n", + "I saw {} field{} in a context where there should be {}.\n", given.if_supports_color(Stdout, |s| s.purple()), + if *given <= 1 { "" } else { "s"}, expected.if_supports_color(Stdout, |s| s.purple()), )] #[diagnostic(url("https://aiken-lang.org/language-tour/custom-types"))] @@ -744,7 +745,7 @@ Perhaps, try the following: #[diagnostic(code("unknown::record_field"))] #[diagnostic(help( "{}", - suggest_neighbor(label, fields.iter(), "Did you forget to make it public?") + suggest_neighbor(label, fields.iter(), "Did you forget to make it public?\n\nAlso record access is only supported on types with one constructor.") ))] UnknownRecordField { #[label] diff --git a/crates/aiken-lang/src/uplc.rs b/crates/aiken-lang/src/uplc.rs index be6e086e..96efc39a 100644 --- a/crates/aiken-lang/src/uplc.rs +++ b/crates/aiken-lang/src/uplc.rs @@ -26,7 +26,7 @@ use crate::{ builder::{ check_replaceable_opaque_type, check_when_pattern_needs, constants_ir, convert_constants_to_data, convert_data_to_type, convert_type_to_data, get_common_ancestor, - get_generics_and_type, handle_clause_guard, handle_func_dependencies_ir, + get_generic_id_and_type, handle_clause_guard, handle_func_dependencies_ir, handle_recursion_ir, list_access_to_uplc, lookup_data_type_by_tipo, monomorphize, rearrange_clauses, replace_opaque_type, wrap_validator_args, AssignmentProperties, ClauseProperties, DataTypeKey, FuncComponents, FunctionAccessKey, @@ -2056,66 +2056,78 @@ impl<'a> CodeGenerator<'a> { scope: scope.clone(), check_last_item: false, }); - } else { + } else if !tipo.is_bool() { pattern_vec.push(Air::Let { scope: scope.clone(), name: "_".to_string(), }); } - if matches!(assignment_properties.kind, AssignmentKind::Expect) { - let data_type = - lookup_data_type_by_tipo(self.data_types.clone(), tipo).unwrap(); + match assignment_properties.kind { + AssignmentKind::Let => { + pattern_vec.append(values); + } + AssignmentKind::Expect => { + if tipo.is_bool() { + pattern_vec.push(Air::AssertBool { + scope, + is_true: constr_name == "True", + }); - let (index, _) = data_type - .constructors - .iter() - .enumerate() - .find(|(_, constr)| &constr.name == constr_name) - .unwrap(); + pattern_vec.append(values); + } else { + let data_type = + lookup_data_type_by_tipo(self.data_types.clone(), tipo).unwrap(); - let constr_name = format!("__{}_{}", constr_name, self.id_gen.next()); + let (index, _) = data_type + .constructors + .iter() + .enumerate() + .find(|(_, constr)| &constr.name == constr_name) + .unwrap(); - pattern_vec.push(Air::Let { - scope: scope.clone(), - name: constr_name.clone(), - }); + let constr_name = format!("__{}_{}", constr_name, self.id_gen.next()); - pattern_vec.append(values); + pattern_vec.push(Air::Let { + scope: scope.clone(), + name: constr_name.clone(), + }); - let mut scope = scope; - scope.push(self.id_gen.next()); + pattern_vec.append(values); - pattern_vec.push(Air::AssertConstr { - scope: scope.clone(), - constr_index: index, - }); + let mut scope = scope; + scope.push(self.id_gen.next()); - pattern_vec.push(Air::Var { - scope: scope.clone(), - constructor: ValueConstructor::public( - tipo.clone().into(), - ValueConstructorVariant::LocalVariable { - location: Span::empty(), - }, - ), - name: constr_name.clone(), - variant_name: String::new(), - }); + pattern_vec.push(Air::AssertConstr { + scope: scope.clone(), + constr_index: index, + }); - pattern_vec.push(Air::Var { - scope, - constructor: ValueConstructor::public( - tipo.clone().into(), - ValueConstructorVariant::LocalVariable { - location: Span::empty(), - }, - ), - name: constr_name, - variant_name: String::new(), - }); - } else { - pattern_vec.append(values); + pattern_vec.push(Air::Var { + scope: scope.clone(), + constructor: ValueConstructor::public( + tipo.clone().into(), + ValueConstructorVariant::LocalVariable { + location: Span::empty(), + }, + ), + name: constr_name.clone(), + variant_name: String::new(), + }); + + pattern_vec.push(Air::Var { + scope, + constructor: ValueConstructor::public( + tipo.clone().into(), + ValueConstructorVariant::LocalVariable { + location: Span::empty(), + }, + ), + name: constr_name, + variant_name: String::new(), + }); + } + } } pattern_vec.append(&mut nested_pattern); @@ -3275,18 +3287,24 @@ impl<'a> CodeGenerator<'a> { let param_types = constructor.tipo.arg_types().unwrap(); let mut mono_types: IndexMap> = IndexMap::new(); + let mut map = mono_types.into_iter().collect_vec(); for (index, arg) in function.arguments.iter().enumerate() { if arg.tipo.is_generic() { - let mut map = mono_types.into_iter().collect_vec(); let param_type = ¶m_types[index]; - map.append(&mut get_generics_and_type(&arg.tipo, param_type)); - - mono_types = map.into_iter().collect(); + map.append(&mut get_generic_id_and_type(&arg.tipo, param_type)); } } + if function.return_type.is_generic() { + if let Type::Fn { ret, .. } = &*constructor.tipo { + map.append(&mut get_generic_id_and_type(&function.return_type, ret)) + } + } + + mono_types = map.into_iter().collect(); + let (variant_name, func_ir) = monomorphize(func_ir, mono_types, &constructor.tipo); @@ -3347,23 +3365,32 @@ impl<'a> CodeGenerator<'a> { } else if let (Some(function), Type::Fn { .. }) = (function, &*tipo) { + let param_types = tipo.arg_types().unwrap(); + let mut mono_types: IndexMap> = IndexMap::new(); - - let param_types = tipo.arg_types().unwrap(); + let mut map = mono_types.into_iter().collect_vec(); for (index, arg) in function.arguments.iter().enumerate() { if arg.tipo.is_generic() { - let mut map = mono_types.into_iter().collect_vec(); - map.append(&mut get_generics_and_type( - &arg.tipo, - ¶m_types[index], - )); + let param_type = ¶m_types[index]; - mono_types = map.into_iter().collect(); + map.append(&mut get_generic_id_and_type( + &arg.tipo, param_type, + )); } } + if function.return_type.is_generic() { + if let Type::Fn { ret, .. } = &*constructor.tipo { + map.append(&mut get_generic_id_and_type( + &function.return_type, + ret, + )) + } + } + + mono_types = map.into_iter().collect(); let mut func_ir = vec![]; self.build_ir(&function.body, &mut func_ir, scope.to_vec()); @@ -4310,7 +4337,12 @@ impl<'a> CodeGenerator<'a> { arg_vec.push(arg_stack.pop().unwrap()); } - for arg in arg_vec.iter() { + for (index, arg) in arg_vec.into_iter().enumerate() { + let arg = if matches!(func, DefaultFunction::ChooseData) && index > 0 { + Term::Delay(arg.into()) + } else { + arg + }; term = apply_wrap(term, arg.clone()); } @@ -4425,6 +4457,58 @@ impl<'a> CodeGenerator<'a> { DefaultFunction::MkCons => { unimplemented!("Use brackets instead."); } + DefaultFunction::IfThenElse + | DefaultFunction::ChooseList + | DefaultFunction::Trace => unimplemented!("{func:#?}"), + DefaultFunction::ChooseData => { + let temp_vars = (0..func.arity()) + .into_iter() + .map(|_| format!("__item_{}", self.id_gen.next())) + .collect_vec(); + + if count == 0 { + for (index, temp_var) in temp_vars.iter().enumerate() { + term = apply_wrap( + term, + if index > 0 { + Term::Delay( + Term::Var( + Name { + text: temp_var.clone(), + unique: 0.into(), + } + .into(), + ) + .into(), + ) + } else { + Term::Var( + Name { + text: temp_var.clone(), + unique: 0.into(), + } + .into(), + ) + }, + ); + } + } + + term = term.force_wrap(); + + if count == 0 { + for temp_var in temp_vars.into_iter().rev() { + term = Term::Lambda { + parameter_name: Name { + text: temp_var, + unique: 0.into(), + } + .into(), + body: term.into(), + }; + } + } + } _ => {} } arg_stack.push(term); @@ -4828,7 +4912,7 @@ impl<'a> CodeGenerator<'a> { Term::Builtin(DefaultFunction::Trace).force_wrap(), Term::Constant( UplcConstant::String( - "Asserted on incorrect constructor variant.".to_string(), + "Expected on incorrect constructor variant.".to_string(), ) .into(), ), @@ -4851,6 +4935,31 @@ impl<'a> CodeGenerator<'a> { arg_stack.push(term); } + Air::AssertBool { is_true, .. } => { + let value = arg_stack.pop().unwrap(); + let mut term = arg_stack.pop().unwrap(); + + let error_term = apply_wrap( + apply_wrap( + Term::Builtin(DefaultFunction::Trace).force_wrap(), + Term::Constant( + UplcConstant::String( + "Expected on incorrect boolean variant.".to_string(), + ) + .into(), + ), + ), + Term::Delay(Term::Error.into()), + ) + .force_wrap(); + + if is_true { + term = delayed_if_else(value, term, error_term); + } else { + term = delayed_if_else(value, error_term, term); + } + arg_stack.push(term); + } Air::When { subject_name, tipo, .. } => { diff --git a/crates/aiken-project/src/blueprint/schema.rs b/crates/aiken-project/src/blueprint/schema.rs index aecb52fe..855f4dbb 100644 --- a/crates/aiken-project/src/blueprint/schema.rs +++ b/crates/aiken-project/src/blueprint/schema.rs @@ -250,36 +250,22 @@ impl Annotated { Err(Error::new(ErrorContext::UnboundTypeVariable, type_info)) } }, - Type::Tuple { elems } => match &elems[..] { - [left, right] => { - let left = Annotated::from_type(modules, left, type_parameters)? - .into_data(left) - .map_err(|e| e.backtrack(type_info))?; - let right = Annotated::from_type(modules, right, type_parameters)? - .into_data(right) - .map_err(|e| e.backtrack(type_info))?; - Ok(Schema::Data(Data::List(Items::Many(vec![ - left.annotated, - right.annotated, - ]))) - .into()) - } - _ => { - let elems = elems - .iter() - .map(|e| { - Annotated::from_type(modules, e, type_parameters) - .and_then(|s| s.into_data(e).map(|s| s.annotated)) - }) - .collect::, _>>() - .map_err(|e| e.backtrack(type_info))?; - Ok(Annotated { - title: Some("Tuple".to_owned()), - description: None, - annotated: Schema::Data(Data::List(Items::Many(elems))), + Type::Tuple { elems } => { + let elems = elems + .iter() + .map(|e| { + Annotated::from_type(modules, e, type_parameters) + .and_then(|s| s.into_data(e).map(|s| s.annotated)) }) - } - }, + .collect::, _>>() + .map_err(|e| e.backtrack(type_info))?; + + Ok(Annotated { + title: Some("Tuple".to_owned()), + description: None, + annotated: Schema::Data(Data::List(Items::Many(elems))), + }) + } Type::Fn { .. } => Err(Error::new(ErrorContext::UnexpectedFunction, type_info)), } } diff --git a/crates/aiken-project/src/blueprint/validator.rs b/crates/aiken-project/src/blueprint/validator.rs index 2c20353a..0db0d96c 100644 --- a/crates/aiken-project/src/blueprint/validator.rs +++ b/crates/aiken-project/src/blueprint/validator.rs @@ -470,6 +470,7 @@ mod test { "title": "test_module.spend", "hash": "3c6766e7a36df2aa13c0e9e6e071317ed39d05f405771c4f1a81c6cc", "datum": { + "title": "Tuple", "schema": { "dataType": "list", "items": [ diff --git a/crates/aiken-project/src/docs.rs b/crates/aiken-project/src/docs.rs index acdb2103..2c660fa1 100644 --- a/crates/aiken-project/src/docs.rs +++ b/crates/aiken-project/src/docs.rs @@ -575,22 +575,40 @@ fn new_timestamp() -> Duration { } fn find_modules_prefix(modules: &[DocLink]) -> String { - modules + do_find_modules_prefix("", modules) +} + +fn do_find_modules_prefix(current_prefix: &str, modules: &[DocLink]) -> String { + let prefix = modules .iter() .fold(None, |previous_prefix, module| { - let prefix = module - .name - .split('/') - .next() - .unwrap_or_default() - .to_string(); + let name = module.name.strip_prefix(current_prefix).unwrap_or_default(); + let name = if name.starts_with('/') { + name.strip_prefix('/').unwrap_or_default() + } else { + name + }; + + let prefix = name.split('/').next().unwrap_or_default().to_string(); + match previous_prefix { None if prefix != module.name => Some(prefix), Some(..) if Some(prefix) == previous_prefix => previous_prefix, _ => Some(String::new()), } }) - .unwrap_or_default() + .unwrap_or_default(); + + if prefix.is_empty() { + current_prefix.to_string() + } else { + let mut current_prefix = current_prefix.to_owned(); + if !current_prefix.is_empty() { + current_prefix.push('/'); + } + current_prefix.push_str(&prefix); + do_find_modules_prefix(¤t_prefix, modules) + } } #[test] @@ -602,7 +620,7 @@ fn find_modules_prefix_test() { name: "aiken/list".to_string(), path: String::new() }]), - "aiken".to_string() + "aiken/list".to_string() ); assert_eq!( @@ -620,7 +638,7 @@ fn find_modules_prefix_test() { path: String::new() }, DocLink { - name: "aiken/byte_array".to_string(), + name: "aiken/bytearray".to_string(), path: String::new(), } ]), @@ -634,7 +652,56 @@ fn find_modules_prefix_test() { path: String::new() }, DocLink { - name: "foo/byte_array".to_string(), + name: "foo/bytearray".to_string(), + path: String::new(), + } + ]), + "".to_string() + ); +} + +#[test] +fn find_modules_prefix_test_2() { + assert_eq!( + find_modules_prefix(&[ + DocLink { + name: "aiken/trees/bst".to_string(), + path: String::new() + }, + DocLink { + name: "aiken/trees/mt".to_string(), + path: String::new(), + } + ]), + "aiken/trees".to_string() + ); + + assert_eq!( + find_modules_prefix(&[ + DocLink { + name: "aiken/trees/bst".to_string(), + path: String::new() + }, + DocLink { + name: "aiken/trees/mt".to_string(), + path: String::new(), + }, + DocLink { + name: "aiken/sequences".to_string(), + path: String::new(), + } + ]), + "aiken".to_string() + ); + + assert_eq!( + find_modules_prefix(&[ + DocLink { + name: "aiken".to_string(), + path: String::new() + }, + DocLink { + name: "aiken/prelude".to_string(), path: String::new(), } ]), diff --git a/crates/aiken-project/src/lib.rs b/crates/aiken-project/src/lib.rs index ef1d7964..199e1e7e 100644 --- a/crates/aiken-project/src/lib.rs +++ b/crates/aiken-project/src/lib.rs @@ -175,7 +175,10 @@ where let doc_files = docs::generate_all( &self.root, &self.config, - self.checked_modules.values().collect(), + self.checked_modules + .values() + .filter(|CheckedModule { package, .. }| package == &self.config.name.to_string()) + .collect(), ); for file in doc_files { @@ -288,13 +291,13 @@ where verbose, exact_match, } => { - let tests = self.collect_tests(verbose)?; + let tests = self.collect_tests(verbose, match_tests, exact_match)?; if !tests.is_empty() { self.event_listener.handle_event(Event::RunningTests); } - let results = self.eval_scripts(tests, match_tests, exact_match); + let results = self.eval_scripts(tests); let errors: Vec = results .iter() @@ -586,20 +589,84 @@ where Ok(()) } - fn collect_tests(&mut self, verbose: bool) -> Result, Error> { + fn collect_tests( + &mut self, + verbose: bool, + match_tests: Option>, + exact_match: bool, + ) -> Result, Error> { let mut scripts = Vec::new(); - for module in self.checked_modules.values() { - if module.package != self.config.name.to_string() { + + let match_tests = match_tests.map(|mt| { + mt.into_iter() + .map(|match_test| { + let mut match_split_dot = match_test.split('.'); + + let match_module = if match_test.contains('.') || match_test.contains('/') { + match_split_dot.next().unwrap_or("") + } else { + "" + }; + + let match_names = match_split_dot.next().map(|names| { + let names = names.replace(&['{', '}'][..], ""); + + let names_split_comma = names.split(','); + + names_split_comma.map(str::to_string).collect() + }); + + (match_module.to_string(), match_names) + }) + .collect::>)>>() + }); + + for checked_module in self.checked_modules.values() { + if checked_module.package != self.config.name.to_string() { continue; } - for def in module.ast.definitions() { + + for def in checked_module.ast.definitions() { if let Definition::Test(func) = def { - scripts.push((module.input_path.clone(), module.name.clone(), func)) + if let Some(match_tests) = &match_tests { + let is_match = match_tests.iter().any(|(module, names)| { + let matched_module = + module.is_empty() || checked_module.name.contains(module); + + let matched_name = match names { + None => true, + Some(names) => names.iter().any(|name| { + if exact_match { + name == &func.name + } else { + func.name.contains(name) + } + }), + }; + + matched_module && matched_name + }); + + if is_match { + scripts.push(( + checked_module.input_path.clone(), + checked_module.name.clone(), + func, + )) + } + } else { + scripts.push(( + checked_module.input_path.clone(), + checked_module.name.clone(), + func, + )) + } } } } let mut programs = Vec::new(); + for (input_path, module_name, func_def) in scripts { let Function { arguments, @@ -660,12 +727,7 @@ where Ok(programs) } - fn eval_scripts( - &self, - scripts: Vec