From c556ada7d54a6e75fe3b149af8a0a6a8ba26a7a2 Mon Sep 17 00:00:00 2001 From: KtorZ Date: Sat, 15 Mar 2025 15:49:22 +0100 Subject: [PATCH 01/16] introduce namespace enum to distinguish module select from type constructor select in patterns. Signed-off-by: KtorZ --- crates/aiken-lang/src/ast.rs | 27 ++++++++++++------- crates/aiken-lang/src/expr.rs | 13 +++++---- crates/aiken-lang/src/format.rs | 8 +++--- .../snapshots/constructor_module_select.snap | 4 ++- crates/aiken-lang/src/tipo/environment.rs | 14 ++++++---- crates/aiken-lang/src/tipo/error.rs | 13 +++++---- crates/aiken-lang/src/tipo/expr.rs | 18 ++++++------- crates/aiken-lang/src/tipo/pattern.rs | 16 ++++++++--- 8 files changed, 70 insertions(+), 43 deletions(-) diff --git a/crates/aiken-lang/src/ast.rs b/crates/aiken-lang/src/ast.rs index 7d45ab8c..6283737a 100644 --- a/crates/aiken-lang/src/ast.rs +++ b/crates/aiken-lang/src/ast.rs @@ -1535,8 +1535,8 @@ impl BinOp { } } -pub type UntypedPattern = Pattern<(), ()>; -pub type TypedPattern = Pattern>; +pub type UntypedPattern = Pattern<(), (), Namespace>; +pub type TypedPattern = Pattern, String>; impl TypedPattern { pub fn var(name: &str) -> Self { @@ -1654,7 +1654,13 @@ impl TypedPattern { } #[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)] -pub enum Pattern { +pub enum Namespace { + Module(String), + Type(String), +} + +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)] +pub enum Pattern { Int { location: Span, value: String, @@ -1707,7 +1713,7 @@ pub enum Pattern { location: Span, name: String, arguments: Vec>, - module: Option, + module: Option, constructor: Constructor, spread_location: Option, tipo: Type, @@ -1725,7 +1731,7 @@ pub enum Pattern { }, } -impl Pattern { +impl Pattern { pub fn location(&self) -> Span { match self { Pattern::Assign { pattern, .. } => pattern.location(), @@ -2201,22 +2207,23 @@ impl AssignmentKind { } } -pub type MultiPattern = Vec>; +pub type MultiPattern = + Vec>; -pub type UntypedMultiPattern = MultiPattern<(), ()>; -pub type TypedMultiPattern = MultiPattern>; +pub type UntypedMultiPattern = MultiPattern<(), (), Namespace>; +pub type TypedMultiPattern = MultiPattern, String>; #[derive(Debug, Clone, PartialEq)] pub struct UntypedClause { pub location: Span, - pub patterns: Vec1>, + pub patterns: Vec1, pub then: UntypedExpr, } #[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)] pub struct TypedClause { pub location: Span, - pub pattern: Pattern>, + pub pattern: TypedPattern, pub then: TypedExpr, } diff --git a/crates/aiken-lang/src/expr.rs b/crates/aiken-lang/src/expr.rs index 055f39e6..5e4bb154 100644 --- a/crates/aiken-lang/src/expr.rs +++ b/crates/aiken-lang/src/expr.rs @@ -2,16 +2,15 @@ pub(crate) use crate::{ ast::{ self, Annotation, ArgBy, ArgName, AssignmentKind, AssignmentPattern, BinOp, Bls12_381Point, ByteArrayFormatPreference, CallArg, Curve, DataType, DataTypeKey, DefinitionLocation, - Located, LogicalOpChainKind, ParsedCallArg, Pattern, RecordConstructorArg, - RecordUpdateSpread, Span, TraceKind, TypedArg, TypedAssignmentKind, TypedClause, - TypedDataType, TypedIfBranch, TypedPattern, TypedRecordUpdateArg, UnOp, UntypedArg, - UntypedAssignmentKind, UntypedClause, UntypedIfBranch, UntypedRecordUpdateArg, + Located, LogicalOpChainKind, ParsedCallArg, RecordConstructorArg, RecordUpdateSpread, Span, + TraceKind, TypedArg, TypedAssignmentKind, TypedClause, TypedDataType, TypedIfBranch, + TypedPattern, TypedRecordUpdateArg, UnOp, UntypedArg, UntypedAssignmentKind, UntypedClause, + UntypedIfBranch, UntypedRecordUpdateArg, }, parser::token::Base, tipo::{ check_replaceable_opaque_type, convert_opaque_type, lookup_data_type_by_tipo, - ModuleValueConstructor, PatternConstructor, Type, TypeVar, ValueConstructor, - ValueConstructorVariant, + ModuleValueConstructor, Type, TypeVar, ValueConstructor, ValueConstructorVariant, }, }; use indexmap::IndexMap; @@ -109,7 +108,7 @@ pub enum TypedExpr { location: Span, tipo: Rc, value: Box, - pattern: Pattern>, + pattern: TypedPattern, kind: TypedAssignmentKind, }, diff --git a/crates/aiken-lang/src/format.rs b/crates/aiken-lang/src/format.rs index ddee569d..64d7c876 100644 --- a/crates/aiken-lang/src/format.rs +++ b/crates/aiken-lang/src/format.rs @@ -2,7 +2,7 @@ use crate::{ ast::{ Annotation, ArgBy, ArgName, ArgVia, AssignmentKind, AssignmentPattern, BinOp, ByteArrayFormatPreference, CallArg, CurveType, DataType, Definition, Function, - LogicalOpChainKind, ModuleConstant, OnTestFailure, Pattern, RecordConstructor, + LogicalOpChainKind, ModuleConstant, Namespace, OnTestFailure, Pattern, RecordConstructor, RecordConstructorArg, RecordUpdateSpread, Span, TraceKind, TypeAlias, TypedArg, TypedValidator, UnOp, UnqualifiedImport, UntypedArg, UntypedArgVia, UntypedAssignmentKind, UntypedClause, UntypedDefinition, UntypedFunction, UntypedIfBranch, UntypedModule, @@ -1202,7 +1202,7 @@ impl<'comments> Formatter<'comments> { &mut self, name: &'a str, args: &'a [CallArg], - module: &'a Option, + module: &'a Option, spread_location: Option, is_record: bool, ) -> Document<'a> { @@ -1217,7 +1217,9 @@ impl<'comments> Formatter<'comments> { } let name = match module { - Some(m) => m.to_doc().append(".").append(name), + Some(Namespace::Module(m)) | Some(Namespace::Type(m)) => { + m.to_doc().append(".").append(name) + } None => name.to_doc(), }; diff --git a/crates/aiken-lang/src/parser/pattern/snapshots/constructor_module_select.snap b/crates/aiken-lang/src/parser/pattern/snapshots/constructor_module_select.snap index 010bdda3..82a7a1a8 100644 --- a/crates/aiken-lang/src/parser/pattern/snapshots/constructor_module_select.snap +++ b/crates/aiken-lang/src/parser/pattern/snapshots/constructor_module_select.snap @@ -8,7 +8,9 @@ Constructor { name: "Foo", arguments: [], module: Some( - "module", + Module( + "module", + ), ), constructor: (), spread_location: None, diff --git a/crates/aiken-lang/src/tipo/environment.rs b/crates/aiken-lang/src/tipo/environment.rs index 8abec2f6..5c75f3d1 100644 --- a/crates/aiken-lang/src/tipo/environment.rs +++ b/crates/aiken-lang/src/tipo/environment.rs @@ -8,9 +8,9 @@ use super::{ use crate::{ ast::{ self, Annotation, CallArg, DataType, Definition, Function, ModuleConstant, ModuleKind, - RecordConstructor, RecordConstructorArg, Span, TypeAlias, TypedDefinition, TypedFunction, - TypedPattern, TypedValidator, UnqualifiedImport, UntypedArg, UntypedDefinition, - UntypedFunction, Use, Validator, PIPE_VARIABLE, + Namespace, RecordConstructor, RecordConstructorArg, Span, TypeAlias, TypedDefinition, + TypedFunction, TypedPattern, TypedValidator, UnqualifiedImport, UntypedArg, + UntypedDefinition, UntypedFunction, Use, Validator, PIPE_VARIABLE, }, tipo::{fields::FieldMap, TypeAliasAnnotation}, IdGenerator, @@ -443,7 +443,7 @@ impl<'a> Environment<'a> { #[allow(clippy::result_large_err)] pub fn get_value_constructor( &mut self, - module: Option<&String>, + module: Option<&Namespace>, name: &str, location: Span, ) -> Result<&ValueConstructor, Error> { @@ -457,7 +457,11 @@ impl<'a> Environment<'a> { constructors: self.local_constructor_names(), }), - Some(m) => { + Some(Namespace::Type(..)) => { + todo!() + } + + Some(Namespace::Module(m)) => { let (_, module) = self.imported_modules .get(m) diff --git a/crates/aiken-lang/src/tipo/error.rs b/crates/aiken-lang/src/tipo/error.rs index 21c22220..769b05df 100644 --- a/crates/aiken-lang/src/tipo/error.rs +++ b/crates/aiken-lang/src/tipo/error.rs @@ -1,6 +1,9 @@ use super::Type; use crate::{ - ast::{Annotation, BinOp, CallArg, LogicalOpChainKind, Span, UntypedFunction, UntypedPattern}, + ast::{ + Annotation, BinOp, CallArg, LogicalOpChainKind, Namespace, Span, UntypedFunction, + UntypedPattern, + }, error::ExtraData, expr::{self, AssignmentPattern, UntypedAssignmentKind, UntypedExpr}, format::Formatter, @@ -395,7 +398,7 @@ From there, you can define 'increment', a function that takes a single argument expected: usize, given: Vec>, name: String, - module: Option, + module: Option, is_record: bool, }, @@ -718,7 +721,7 @@ Perhaps, try the following: label: String, name: String, args: Vec>, - module: Option, + module: Option, spread_location: Option, }, @@ -1274,7 +1277,7 @@ fn suggest_pattern( expected: usize, name: &str, given: &[CallArg], - module: &Option, + module: &Option, is_record: bool, ) -> Option { if expected > given.len() { @@ -1309,7 +1312,7 @@ fn suggest_generic(name: &str, expected: usize) -> String { fn suggest_constructor_pattern( name: &str, args: &[CallArg], - module: &Option, + module: &Option, spread_location: Option, ) -> String { let fixed_args = args diff --git a/crates/aiken-lang/src/tipo/expr.rs b/crates/aiken-lang/src/tipo/expr.rs index 4c9d959e..e572fa85 100644 --- a/crates/aiken-lang/src/tipo/expr.rs +++ b/crates/aiken-lang/src/tipo/expr.rs @@ -11,11 +11,11 @@ use super::{ use crate::{ ast::{ self, Annotation, ArgName, AssignmentKind, AssignmentPattern, BinOp, Bls12_381Point, - ByteArrayFormatPreference, CallArg, Curve, Function, IfBranch, LogicalOpChainKind, Pattern, - RecordUpdateSpread, Span, TraceKind, TraceLevel, Tracing, TypedArg, TypedCallArg, - TypedClause, TypedIfBranch, TypedPattern, TypedRecordUpdateArg, TypedValidator, UnOp, - UntypedArg, UntypedAssignmentKind, UntypedClause, UntypedFunction, UntypedIfBranch, - UntypedPattern, UntypedRecordUpdateArg, + ByteArrayFormatPreference, CallArg, Curve, Function, IfBranch, LogicalOpChainKind, + Namespace, Pattern, RecordUpdateSpread, Span, TraceKind, TraceLevel, Tracing, TypedArg, + TypedCallArg, TypedClause, TypedIfBranch, TypedPattern, TypedRecordUpdateArg, + TypedValidator, UnOp, UntypedArg, UntypedAssignmentKind, UntypedClause, UntypedFunction, + UntypedIfBranch, UntypedPattern, UntypedRecordUpdateArg, }, builtins::{from_default_function, BUILTIN}, expr::{FnStyle, TypedExpr, UntypedExpr}, @@ -404,7 +404,7 @@ impl<'a, 'b> ExprTyper<'a, 'b> { module_alias, label, .. - } => (Some(module_alias), label), + } => (Some(Namespace::Module(module_alias.to_string())), label), TypedExpr::Var { name, .. } => (None, name), @@ -413,7 +413,7 @@ impl<'a, 'b> ExprTyper<'a, 'b> { Ok(self .environment - .get_value_constructor(module, name, location)? + .get_value_constructor(module.as_ref(), name, location)? .field_map()) } @@ -792,12 +792,12 @@ impl<'a, 'b> ExprTyper<'a, 'b> { args: Vec, location: Span, ) -> Result { - let (module, name): (Option, String) = match self.infer(constructor.clone())? { + let (module, name): (Option, String) = match self.infer(constructor.clone())? { TypedExpr::ModuleSelect { module_alias, label, .. - } => (Some(module_alias), label), + } => (Some(Namespace::Module(module_alias)), label), TypedExpr::Var { name, .. } => (None, name), diff --git a/crates/aiken-lang/src/tipo/pattern.rs b/crates/aiken-lang/src/tipo/pattern.rs index 99ad74ef..d3a23afc 100644 --- a/crates/aiken-lang/src/tipo/pattern.rs +++ b/crates/aiken-lang/src/tipo/pattern.rs @@ -6,7 +6,7 @@ use super::{ hydrator::Hydrator, PatternConstructor, Type, ValueConstructorVariant, }; -use crate::ast::{CallArg, Pattern, Span, TypedPattern, UntypedPattern}; +use crate::ast::{CallArg, Namespace, Pattern, Span, TypedPattern, UntypedPattern}; use itertools::Itertools; use std::{ collections::{HashMap, HashSet}, @@ -570,7 +570,12 @@ impl<'a, 'b> PatternTyper<'a, 'b> { Ok(Pattern::Constructor { location, - module, + // NOTE: + // Type namespaces are completely erased during type-check. + module: match module { + None | Some(Namespace::Type(_)) => None, + Some(Namespace::Module(m)) => Some(m), + }, name, arguments: pattern_args, constructor, @@ -601,7 +606,12 @@ impl<'a, 'b> PatternTyper<'a, 'b> { Ok(Pattern::Constructor { location, - module, + // NOTE: + // Type namespaces are completely erased during type-check. + module: match module { + None | Some(Namespace::Type(_)) => None, + Some(Namespace::Module(m)) => Some(m), + }, name, arguments: vec![], constructor, From 81713d93a00e8216e1e0232f9e9ea608054bb0c8 Mon Sep 17 00:00:00 2001 From: KtorZ Date: Sat, 15 Mar 2025 16:57:59 +0100 Subject: [PATCH 02/16] allow types to be used as namespace in patterns. Signed-off-by: KtorZ --- .../src/parser/chain/field_access.rs | 3 +- .../src/parser/pattern/constructor.rs | 61 ++++++++---- .../snapshots/constructor_type_select.snap | 18 ++++ crates/aiken-lang/src/parser/pattern/var.rs | 54 ++++++----- crates/aiken-lang/src/tests/check.rs | 94 +++++++++++++++++++ crates/aiken-lang/src/tipo/environment.rs | 60 +++++++++++- 6 files changed, 241 insertions(+), 49 deletions(-) create mode 100644 crates/aiken-lang/src/parser/pattern/snapshots/constructor_type_select.snap diff --git a/crates/aiken-lang/src/parser/chain/field_access.rs b/crates/aiken-lang/src/parser/chain/field_access.rs index 86eab783..24ee5e7b 100644 --- a/crates/aiken-lang/src/parser/chain/field_access.rs +++ b/crates/aiken-lang/src/parser/chain/field_access.rs @@ -1,5 +1,6 @@ use super::Chain; use crate::{ + ast::well_known, expr::UntypedExpr, parser::{token::Token, ParseError}, }; @@ -8,7 +9,7 @@ use chumsky::prelude::*; pub(crate) fn parser() -> impl Parser { just(Token::Dot) .ignore_then(choice(( - select! { Token::Else => "else".to_string() }, + select! { Token::Else => well_known::VALIDATOR_ELSE.to_string() }, select! { Token::Name { name } => name, }, ))) .map_with_span(Chain::FieldAccess) diff --git a/crates/aiken-lang/src/parser/pattern/constructor.rs b/crates/aiken-lang/src/parser/pattern/constructor.rs index a0336fd2..ebfa681e 100644 --- a/crates/aiken-lang/src/parser/pattern/constructor.rs +++ b/crates/aiken-lang/src/parser/pattern/constructor.rs @@ -1,29 +1,51 @@ use chumsky::prelude::*; use crate::{ - ast::{CallArg, Span, UntypedPattern}, + ast::{CallArg, Namespace, Span, UntypedPattern}, parser::{error::ParseError, token::Token}, }; pub fn parser( pattern: Recursive<'_, Token, UntypedPattern, ParseError>, ) -> impl Parser + '_ { - select! {Token::UpName { name } => name} - .then(args(pattern)) - .map_with_span( - |(name, (arguments, spread_location, is_record)), location| { - UntypedPattern::Constructor { - is_record, - location, - name, - arguments, - module: None, - constructor: (), - spread_location, - tipo: (), - } - }, - ) + choice(( + select! { Token::UpName { name } => name } + .then( + just(Token::Dot).ignore_then( + select! {Token::UpName { name } => name}.then(args(pattern.clone())), + ), + ) + .map_with_span( + |(namespace, (name, (arguments, spread_location, is_record))), span| { + UntypedPattern::Constructor { + is_record, + location: span, + name, + arguments, + module: Some(Namespace::Type(namespace)), + constructor: (), + spread_location, + tipo: (), + } + }, + ), + select! {Token::UpName { name } => name} + .then(args(pattern)) + .map_with_span( + |(name, (arguments, spread_location, is_record)), location| { + UntypedPattern::Constructor { + is_record, + location, + name, + arguments, + module: None, + constructor: (), + spread_location, + tipo: (), + } + }, + ), + )) } pub(crate) fn args( @@ -102,4 +124,9 @@ mod tests { fn constructor_module_select() { assert_pattern!("module.Foo"); } + + #[test] + fn constructor_type_select() { + assert_pattern!("Foo.Bar"); + } } diff --git a/crates/aiken-lang/src/parser/pattern/snapshots/constructor_type_select.snap b/crates/aiken-lang/src/parser/pattern/snapshots/constructor_type_select.snap new file mode 100644 index 00000000..468e6816 --- /dev/null +++ b/crates/aiken-lang/src/parser/pattern/snapshots/constructor_type_select.snap @@ -0,0 +1,18 @@ +--- +source: crates/aiken-lang/src/parser/pattern/constructor.rs +description: "Code:\n\nFoo.Bar" +--- +Constructor { + is_record: false, + location: 0..7, + name: "Bar", + arguments: [], + module: Some( + Type( + "Foo", + ), + ), + constructor: (), + spread_location: None, + tipo: (), +} diff --git a/crates/aiken-lang/src/parser/pattern/var.rs b/crates/aiken-lang/src/parser/pattern/var.rs index c278f825..a75bc87f 100644 --- a/crates/aiken-lang/src/parser/pattern/var.rs +++ b/crates/aiken-lang/src/parser/pattern/var.rs @@ -9,31 +9,33 @@ use crate::{ pub fn parser( expression: Recursive<'_, Token, UntypedPattern, ParseError>, ) -> impl Parser + '_ { - select! { Token::Name {name} => name } - .then( - just(Token::Dot) - .ignore_then( - select! {Token::UpName { name } => name}.then(constructor::args(expression)), - ) - .or_not(), - ) - .map_with_span(|(name, opt_pattern), span| { - if let Some((c_name, (arguments, spread_location, is_record))) = opt_pattern { - UntypedPattern::Constructor { - is_record, - location: span, - name: c_name, - arguments, - module: Some(name), - constructor: (), - spread_location, - tipo: (), - } - } else { - UntypedPattern::Var { - location: span, - name, - } + select! { + Token::Name {name} => name, + } + .then( + just(Token::Dot) + .ignore_then( + select! { Token::UpName { name } => name }.then(constructor::args(expression)), + ) + .or_not(), + ) + .map_with_span(|(name, opt_pattern), span| { + if let Some((c_name, (arguments, spread_location, is_record))) = opt_pattern { + UntypedPattern::Constructor { + is_record, + location: span, + name: c_name, + arguments, + module: Some(crate::ast::Namespace::Module(name)), + constructor: (), + spread_location, + tipo: (), } - }) + } else { + UntypedPattern::Var { + location: span, + name, + } + } + }) } diff --git a/crates/aiken-lang/src/tests/check.rs b/crates/aiken-lang/src/tests/check.rs index 9410b801..4fb8617c 100644 --- a/crates/aiken-lang/src/tests/check.rs +++ b/crates/aiken-lang/src/tests/check.rs @@ -2254,6 +2254,100 @@ fn allow_expect_into_opaque_type_constructor_without_typecasting_in_module() { assert!(check(parse(source_code)).is_ok()); } +#[test] +fn use_type_as_namespace_for_patterns() { + let dependency = r#" + pub type Foo { + A + B + } + "#; + + let source_code = r#" + use thing.{Foo} + + pub fn bar(foo: Foo) { + when foo is { + Foo.A -> True + Foo.B -> False + } + } + "#; + + let result = check_with_deps(parse(source_code), vec![(parse_as(dependency, "thing"))]); + + assert!(matches!(result, Ok(..)), "{result:#?}"); +} + +#[test] +fn use_opaque_type_as_namespace_for_patterns_fails() { + let dependency = r#" + pub opaque type Foo { + A + B + } + "#; + + let source_code = r#" + use thing.{Foo} + + fn bar(foo: Foo) { + when foo is { + Foo.A -> True + Foo.B -> False + } + } + "#; + + let result = check_with_deps(parse(source_code), vec![(parse_as(dependency, "thing"))]); + + assert!( + matches!( + &result, + Err((warnings, Error::UnknownTypeConstructor { name, .. })) if name == "A" && warnings.is_empty(), + ), + "{result:#?}" + ); +} + +#[test] +fn use_wrong_type_as_namespace_for_patterns_fails() { + let dependency = r#" + pub type Foo { + A + B + } + + pub type Bar { + C + D + } + + + "#; + + let source_code = r#" + use thing.{Foo} + + fn bar(foo: Foo) { + when foo is { + Foo.A -> True + Foo.D -> False + } + } + "#; + + let result = check_with_deps(parse(source_code), vec![(parse_as(dependency, "thing"))]); + + assert!( + matches!( + &result, + Err((warnings, Error::UnknownTypeConstructor { name, .. })) if name == "D" && warnings.is_empty(), + ), + "{result:#?}" + ); +} + #[test] fn forbid_importing_or_using_opaque_constructors() { let dependency = r#" diff --git a/crates/aiken-lang/src/tipo/environment.rs b/crates/aiken-lang/src/tipo/environment.rs index 5c75f3d1..988365de 100644 --- a/crates/aiken-lang/src/tipo/environment.rs +++ b/crates/aiken-lang/src/tipo/environment.rs @@ -378,7 +378,7 @@ impl<'a> Environment<'a> { name: &str, location: Span, ) -> Result<&mut TypeConstructor, Error> { - let types = self.module_types.keys().map(|t| t.to_string()).collect(); + let types = self.known_type_names(); let constructor = self .module_types @@ -407,7 +407,7 @@ impl<'a> Environment<'a> { .ok_or_else(|| Error::UnknownType { location, name: name.to_string(), - types: self.module_types.keys().map(|t| t.to_string()).collect(), + types: self.known_type_names(), }), Some(m) => { @@ -457,8 +457,42 @@ impl<'a> Environment<'a> { constructors: self.local_constructor_names(), }), - Some(Namespace::Type(..)) => { - todo!() + Some(Namespace::Type(t)) => { + let parent_type = self.module_types.get(t).ok_or_else(|| Error::UnknownType { + location, + name: t.to_string(), + types: self.known_type_names(), + })?; + + let (_, module) = + self.imported_modules + .get(&parent_type.module) + .ok_or_else(|| Error::UnknownModule { + name: parent_type.module.to_string(), + known_modules: self + .importable_modules + .keys() + .map(|t| t.to_string()) + .collect(), + location, + })?; + + self.unused_modules.remove(&parent_type.module); + + let empty_vec = vec![]; + let constructors = module.types_constructors.get(t).unwrap_or(&empty_vec); + + let unknown_type_constructor = || Error::UnknownTypeConstructor { + location, + name: name.to_string(), + constructors: constructors.clone(), + }; + + if !constructors.iter().any(|constructor| constructor == name) { + return Err(unknown_type_constructor()); + } + + module.values.get(name).ok_or_else(unknown_type_constructor) } Some(Namespace::Module(m)) => { @@ -732,6 +766,22 @@ impl<'a> Environment<'a> { } } + /// Get a list of known type names, for suggestions in errors. + pub fn known_type_names(&self) -> Vec { + self.module_types + .keys() + .filter_map(|t| { + // Avoid leaking special internal types such as __ScriptContext or + // __ScriptPurpose. + if t.starts_with("__") { + None + } else { + Some(t.to_string()) + } + }) + .collect() + } + pub fn local_value_names(&self) -> Vec { self.scope .keys() @@ -1820,7 +1870,7 @@ impl<'a> Environment<'a> { .get(name) .ok_or_else(|| Error::UnknownType { name: name.to_string(), - types: self.module_types.keys().map(|t| t.to_string()).collect(), + types: self.known_type_names(), location, })? .iter() From 79a7d80c7feb47d404f5102ad3a2a2ccd11bae35 Mon Sep 17 00:00:00 2001 From: KtorZ Date: Sat, 15 Mar 2025 16:59:53 +0100 Subject: [PATCH 03/16] Fix dependency namespacing in 'checks' tests. We were mixing module names and package names in a very weird way but it was somehow working. However, the generated ASTs would vastly differ from what we would typically get from running those tests on a standard project -- which defies a bit the purpose of those tests. Signed-off-by: KtorZ --- crates/aiken-lang/src/tests/check.rs | 41 ++++++++++++++++------------ 1 file changed, 24 insertions(+), 17 deletions(-) diff --git a/crates/aiken-lang/src/tests/check.rs b/crates/aiken-lang/src/tests/check.rs index 4fb8617c..d9a7b2b8 100644 --- a/crates/aiken-lang/src/tests/check.rs +++ b/crates/aiken-lang/src/tests/check.rs @@ -11,15 +11,23 @@ use crate::{ }; use std::collections::HashMap; +const DEFAULT_MODULE_NAME: &str = "my_module"; +const DEFAULT_PACKAGE: &str = "test/project"; + fn parse(source_code: &str) -> UntypedModule { + parse_as(source_code, DEFAULT_MODULE_NAME) +} + +fn parse_as(source_code: &str, name: &str) -> UntypedModule { let kind = ModuleKind::Lib; - let (ast, _) = parser::module(source_code, kind).expect("Failed to parse module"); + let (mut ast, _) = parser::module(source_code, kind).expect("Failed to parse module"); + ast.name = name.to_string(); ast } fn check_module( ast: UntypedModule, - extra: Vec<(String, UntypedModule)>, + extra: Vec, kind: ModuleKind, tracing: Tracing, ) -> Result<(Vec, TypedModule), (Vec, Error)> { @@ -31,27 +39,32 @@ fn check_module( module_types.insert("aiken".to_string(), builtins::prelude(&id_gen)); module_types.insert("aiken/builtin".to_string(), builtins::plutus(&id_gen)); - for (package, module) in extra { + for module in extra { let mut warnings = vec![]; + if module.name == DEFAULT_MODULE_NAME { + panic!("passed extra modules with default name! Use 'parse_as' to define tests instead of 'parse'."); + } + let typed_module = module .infer( &id_gen, kind, - &package, + DEFAULT_PACKAGE, &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()); + + module_types.insert(typed_module.name.clone(), typed_module.type_info.clone()); } let result = ast.infer( &id_gen, kind, - "test/project", + DEFAULT_PACKAGE, &module_types, tracing, &mut warnings, @@ -76,7 +89,7 @@ fn check_with_verbosity( fn check_with_deps( ast: UntypedModule, - extra: Vec<(String, UntypedModule)>, + extra: Vec, ) -> Result<(Vec, TypedModule), (Vec, Error)> { check_module(ast, extra, ModuleKind::Lib, Tracing::verbose()) } @@ -2358,7 +2371,7 @@ fn forbid_importing_or_using_opaque_constructors() { "#; let source_code = r#" - use foo/thing.{Thing, Foo} + use thing.{Thing, Foo} fn bar(thing: Thing) { expect Foo(a) = thing @@ -2367,15 +2380,12 @@ fn forbid_importing_or_using_opaque_constructors() { "#; assert!(matches!( - check_with_deps( - parse(source_code), - vec![("foo/thing".to_string(), parse(dependency))], - ), + check_with_deps(parse(source_code), vec![(parse_as(dependency, "thing"))],), Err((_, Error::UnknownModuleField { .. })), )); let source_code = r#" - use foo/thing.{Thing} + use thing.{Thing} fn bar(thing: Thing) { expect Foo(a) = thing @@ -2384,10 +2394,7 @@ fn forbid_importing_or_using_opaque_constructors() { "#; assert!(matches!( - check_with_deps( - parse(source_code), - vec![("foo/thing".to_string(), parse(dependency))], - ), + check_with_deps(parse(source_code), vec![(parse_as(dependency, "thing"))],), Err((_, Error::UnknownTypeConstructor { .. })), )); } From c66bca582953c8d7550188a4585f2e8052b8ed23 Mon Sep 17 00:00:00 2001 From: KtorZ Date: Sat, 15 Mar 2025 17:00:05 +0100 Subject: [PATCH 04/16] remove unused value constructors from modules type info This is a mildly scaring change, as it may horribly break things down the line if we have ever relied on that information... However, the value constructors inside each module are meant to reflect the own module public API, and shouldn't be populated with Prelude stuff. Not only does that mean this information is duplicated for all modules (possibly significantly slowing things down), but it may also cause weird scoping issues (e.g. one accessing a Prelude's constructor through some random module). Hence why I am isolating this change in this single commit, so it's easier to troubleshoot if that ends up causing weird issues. Signed-off-by: KtorZ --- crates/aiken-lang/src/tipo/infer.rs | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/crates/aiken-lang/src/tipo/infer.rs b/crates/aiken-lang/src/tipo/infer.rs index a8945811..a148a6a9 100644 --- a/crates/aiken-lang/src/tipo/infer.rs +++ b/crates/aiken-lang/src/tipo/infer.rs @@ -17,7 +17,12 @@ use crate::{ tipo::{expr::infer_function, Span, Type, TypeVar}, IdGenerator, }; -use std::{borrow::Borrow, collections::HashMap, ops::Deref, rc::Rc}; +use std::{ + borrow::Borrow, + collections::{BTreeSet, HashMap}, + ops::Deref, + rc::Rc, +}; impl UntypedModule { #[allow(clippy::too_many_arguments)] @@ -116,8 +121,14 @@ impl UntypedModule { .module_types .retain(|_, info| info.public && info.module == module_name); + let own_types = environment.module_types.keys().collect::>(); + environment.module_values.retain(|_, info| info.public); + environment + .module_types_constructors + .retain(|k, _| own_types.contains(k)); + environment .accessors .retain(|_, accessors| accessors.public); From 3db9828fe8978c356a580fd7a534b36186ae565f Mon Sep 17 00:00:00 2001 From: KtorZ Date: Sat, 15 Mar 2025 21:11:08 +0100 Subject: [PATCH 05/16] allow use of types as namespaces for constructing values. Signed-off-by: KtorZ --- .../src/parser/chain/field_access.rs | 17 +- crates/aiken-lang/src/parser/expr/chained.rs | 1 - crates/aiken-lang/src/tests/check.rs | 235 +++++++++++++++++- crates/aiken-lang/src/tipo/environment.rs | 79 ++++-- crates/aiken-lang/src/tipo/expr.rs | 85 ++++++- 5 files changed, 367 insertions(+), 50 deletions(-) diff --git a/crates/aiken-lang/src/parser/chain/field_access.rs b/crates/aiken-lang/src/parser/chain/field_access.rs index 24ee5e7b..05c0708a 100644 --- a/crates/aiken-lang/src/parser/chain/field_access.rs +++ b/crates/aiken-lang/src/parser/chain/field_access.rs @@ -1,7 +1,6 @@ use super::Chain; use crate::{ ast::well_known, - expr::UntypedExpr, parser::{token::Token, ParseError}, }; use chumsky::prelude::*; @@ -11,25 +10,11 @@ pub(crate) fn parser() -> impl Parser { .ignore_then(choice(( select! { Token::Else => well_known::VALIDATOR_ELSE.to_string() }, select! { Token::Name { name } => name, }, + select! { Token::UpName { name } => name, }, ))) .map_with_span(Chain::FieldAccess) } -pub(crate) fn constructor() -> impl Parser { - select! {Token::Name { name } => name} - .map_with_span(|module, span| (module, span)) - .then_ignore(just(Token::Dot)) - .then(select! {Token::UpName { name } => name}) - .map_with_span(|((module, m_span), name), span| UntypedExpr::FieldAccess { - location: span, - label: name, - container: Box::new(UntypedExpr::Var { - location: m_span, - name: module, - }), - }) -} - #[cfg(test)] mod tests { use crate::assert_expr; diff --git a/crates/aiken-lang/src/parser/expr/chained.rs b/crates/aiken-lang/src/parser/expr/chained.rs index 13ea7940..2ac3ee32 100644 --- a/crates/aiken-lang/src/parser/expr/chained.rs +++ b/crates/aiken-lang/src/parser/expr/chained.rs @@ -53,7 +53,6 @@ pub fn chain_start<'a>( pair(expression.clone()), record_update(expression.clone()), record(expression.clone()), - field_access::constructor(), and_or_chain(expression.clone()), var(), tuple(expression.clone()), diff --git a/crates/aiken-lang/src/tests/check.rs b/crates/aiken-lang/src/tests/check.rs index d9a7b2b8..c8e67fda 100644 --- a/crates/aiken-lang/src/tests/check.rs +++ b/crates/aiken-lang/src/tests/check.rs @@ -2279,7 +2279,7 @@ fn use_type_as_namespace_for_patterns() { let source_code = r#" use thing.{Foo} - pub fn bar(foo: Foo) { + fn bar(foo: Foo) { when foo is { Foo.A -> True Foo.B -> False @@ -2335,8 +2335,6 @@ fn use_wrong_type_as_namespace_for_patterns_fails() { C D } - - "#; let source_code = r#" @@ -2361,6 +2359,237 @@ fn use_wrong_type_as_namespace_for_patterns_fails() { ); } +#[test] +fn use_type_as_namespace_for_constructors() { + let dependency = r#" + pub type Foo { + I(Int) + B(Bool) + } + "#; + + let source_code = r#" + use foo.{Foo} + + test my_test() { + and { + Foo.I(42) == foo.I(42), + foo.B(True) == Foo.B(True), + } + } + "#; + + let result = check_with_deps(parse(source_code), vec![(parse_as(dependency, "foo"))]); + + assert!(matches!(result, Ok(..)), "{result:#?}"); +} + +#[test] +fn use_type_as_nested_namespace_for_constructors() { + let dependency = r#" + pub type Foo { + I(Int) + B(Bool) + } + "#; + + let source_code = r#" + use foo + + test my_test() { + trace foo.Foo.I(42) + Void + } + "#; + + let result = check_with_deps(parse(source_code), vec![(parse_as(dependency, "foo"))]); + + assert!(matches!(result, Ok(..)), "{result:#?}"); +} + +#[test] +fn use_type_as_namespace_unknown_constructor() { + let dependency = r#" + pub type Foo { + I(Int) + B(Bool) + } + "#; + + let source_code = r#" + use foo.{Foo} + + test my_test() { + Foo.A == Foo.I(42) + } + "#; + + let result = check_with_deps(parse(source_code), vec![(parse_as(dependency, "foo"))]); + + assert!( + matches!( + &result, + Err((warnings, Error::UnknownTypeConstructor { name, .. })) if name == "A" && warnings.is_empty(), + ), + "{result:#?}" + ); +} + +#[test] +fn use_wrong_type_as_namespace() { + let dependency = r#" + pub type Foo { + I(Int) + B(Bool) + } + + pub type Bar { + S(String) + L(List) + } + "#; + + let source_code = r#" + use foo.{Foo} + + test my_test() { + trace Foo.S(@"wut") + Void + } + "#; + + let result = check_with_deps(parse(source_code), vec![(parse_as(dependency, "foo"))]); + + assert!( + matches!( + &result, + Err((warnings, Error::UnknownTypeConstructor { name, .. })) if name == "S" && warnings.is_empty(), + ), + "{result:#?}" + ); +} + +#[test] +fn use_wrong_nested_type_as_namespace() { + let dependency = r#" + pub type Foo { + I(Int) + B(Bool) + } + + pub type Bar { + S(String) + L(List) + } + "#; + + let source_code = r#" + use foo + + test my_test() { + trace foo.Foo.S(@"wut") + Void + } + "#; + + let result = check_with_deps(parse(source_code), vec![(parse_as(dependency, "foo"))]); + + assert!( + matches!( + &result, + Err((warnings, Error::UnknownTypeConstructor { name, .. })) if name == "S" && warnings.is_empty(), + ), + "{result:#?}" + ); +} + +#[test] +fn use_private_type_as_nested_namespace_fails() { + let dependency = r#" + type Foo { + I(Int) + B(Bool) + } + "#; + + let source_code = r#" + use foo + + test my_test() { + trace foo.Foo.I(42) + Void + } + "#; + + let result = check_with_deps(parse(source_code), vec![(parse_as(dependency, "foo"))]); + + assert!( + matches!( + &result, + Err((warnings, Error::UnknownModuleType { name, .. })) if name == "Foo" && warnings.is_empty(), + ), + "{result:#?}" + ); +} + +#[test] +fn use_opaque_type_as_namespace_for_constructors_fails() { + let dependency = r#" + pub opaque type Foo { + I(Int) + B(Bool) + } + "#; + + let source_code = r#" + use foo.{Foo} + + test my_test() { + trace Foo.I(42) + Void + } + "#; + + let result = check_with_deps(parse(source_code), vec![(parse_as(dependency, "foo"))]); + + assert!( + matches!( + &result, + Err((warnings, Error::UnknownTypeConstructor { name, .. })) if name == "I" && warnings.is_empty(), + ), + "{result:#?}" + ); +} + +#[test] +fn use_opaque_type_as_nested_namespace_for_constructors_fails() { + let dependency = r#" + pub opaque type Foo { + I(Int) + B(Bool) + } + "#; + + let source_code = r#" + use foo + + test my_test() { + trace foo.Foo.I(42) + Void + } + "#; + + let result = check_with_deps(parse(source_code), vec![(parse_as(dependency, "foo"))]); + + assert!( + matches!( + &result, + Err((warnings, Error::UnknownTypeConstructor { name, .. })) if name == "I" && warnings.is_empty(), + ), + "{result:#?}" + ); +} + #[test] fn forbid_importing_or_using_opaque_constructors() { let dependency = r#" diff --git a/crates/aiken-lang/src/tipo/environment.rs b/crates/aiken-lang/src/tipo/environment.rs index 988365de..6473ce91 100644 --- a/crates/aiken-lang/src/tipo/environment.rs +++ b/crates/aiken-lang/src/tipo/environment.rs @@ -372,6 +372,53 @@ impl<'a> Environment<'a> { } } + #[allow(clippy::result_large_err)] + pub fn get_fully_qualified_value_constructor( + &self, + (module_name, module_location): (&str, Span), + (type_name, type_location): (&str, Span), + (value, value_location): (&str, Span), + ) -> Result<&ValueConstructor, Error> { + let (_, module) = + self.imported_modules + .get(module_name) + .ok_or_else(|| Error::UnknownModule { + location: module_location, + name: module_name.to_string(), + known_modules: self + .importable_modules + .keys() + .map(|t| t.to_string()) + .collect(), + })?; + + let constructors = + module + .types_constructors + .get(type_name) + .ok_or_else(|| Error::UnknownModuleType { + location: type_location, + name: type_name.to_string(), + module_name: module.name.clone(), + type_constructors: module.types.keys().map(|t| t.to_string()).collect(), + })?; + + let unknown_type_constructor = || Error::UnknownTypeConstructor { + location: value_location, + name: value.to_string(), + constructors: constructors.clone(), + }; + + if !constructors.iter().any(|s| s.as_str() == value) { + return Err(unknown_type_constructor()); + } + + module + .values + .get(value) + .ok_or_else(unknown_type_constructor) + } + #[allow(clippy::result_large_err)] pub fn get_type_constructor_mut( &mut self, @@ -464,35 +511,13 @@ impl<'a> Environment<'a> { types: self.known_type_names(), })?; - let (_, module) = - self.imported_modules - .get(&parent_type.module) - .ok_or_else(|| Error::UnknownModule { - name: parent_type.module.to_string(), - known_modules: self - .importable_modules - .keys() - .map(|t| t.to_string()) - .collect(), - location, - })?; - self.unused_modules.remove(&parent_type.module); - let empty_vec = vec![]; - let constructors = module.types_constructors.get(t).unwrap_or(&empty_vec); - - let unknown_type_constructor = || Error::UnknownTypeConstructor { - location, - name: name.to_string(), - constructors: constructors.clone(), - }; - - if !constructors.iter().any(|constructor| constructor == name) { - return Err(unknown_type_constructor()); - } - - module.values.get(name).ok_or_else(unknown_type_constructor) + self.get_fully_qualified_value_constructor( + (parent_type.module.as_str(), location), + (t, location), + (name, location.map(|start, end| (start + t.len() + 1, end))), + ) } Some(Namespace::Module(m)) => { diff --git a/crates/aiken-lang/src/tipo/expr.rs b/crates/aiken-lang/src/tipo/expr.rs index e572fa85..483a9839 100644 --- a/crates/aiken-lang/src/tipo/expr.rs +++ b/crates/aiken-lang/src/tipo/expr.rs @@ -1068,6 +1068,41 @@ impl<'a, 'b> ExprTyper<'a, 'b> { return shortcircuit; } + // In case where we find an uppercase var name in a record access chain, we treat the type + // as a namespace and lookup the next constructor as if it were imported from the module's + // the type originally belong to. + match container { + UntypedExpr::Var { + name: ref type_name, + location: type_location, + } if TypeConstructor::might_be(type_name) => { + return self.infer_type_constructor_access( + (type_name, type_location), + (&label, access_location), + ); + } + + UntypedExpr::FieldAccess { + location: type_location, + label: ref type_name, + container: ref type_container, + } if TypeConstructor::might_be(type_name) => { + if let UntypedExpr::Var { + name: ref module_name, + location: module_location, + } = type_container.as_ref() + { + return self.infer_inner_type_constructor_access( + (module_name, *module_location), + (type_name, type_location), + (&label, access_location), + ); + } + } + + _ => (), + }; + // Attempt to infer the container as a record access. If that fails, we may be shadowing the name // of an imported module, so attempt to infer the container as a module access. // TODO: Remove this cloning @@ -1075,7 +1110,7 @@ impl<'a, 'b> ExprTyper<'a, 'b> { Ok(record_access) => Ok(record_access), Err(err) => match container { - UntypedExpr::Var { name, location } => { + UntypedExpr::Var { name, location } if !TypeConstructor::might_be(&name) => { let module_access = self.infer_module_access(&name, label, &location, access_location); @@ -1162,6 +1197,51 @@ impl<'a, 'b> ExprTyper<'a, 'b> { }) } + #[allow(clippy::result_large_err)] + fn infer_type_constructor_access( + &mut self, + (type_name, type_location): (&str, Span), + (label, label_location): (&str, Span), + ) -> Result { + let parent_type = self + .environment + .module_types + .get(type_name) + .ok_or_else(|| Error::UnknownType { + location: type_location, + name: type_name.to_string(), + types: self.environment.known_type_names(), + })?; + + let module_name = parent_type.module.clone(); + + self.infer_inner_type_constructor_access( + (module_name.as_str(), type_location), + (type_name, type_location), + (label, label_location), + ) + } + + #[allow(clippy::result_large_err)] + fn infer_inner_type_constructor_access( + &mut self, + (module_name, module_location): (&str, Span), + (type_name, type_location): (&str, Span), + (label, label_location): (&str, Span), + ) -> Result { + self.environment.get_fully_qualified_value_constructor( + (module_name, module_location), + (type_name, type_location), + (label, label_location), + )?; + self.infer_module_access( + module_name, + label.to_string(), + &type_location, + label_location, + ) + } + #[allow(clippy::result_large_err)] fn infer_record_access( &mut self, @@ -1169,9 +1249,8 @@ impl<'a, 'b> ExprTyper<'a, 'b> { label: String, location: Span, ) -> Result { - // Infer the type of the (presumed) record + // Infer the type of the (presumed) record. let record = self.infer(record)?; - self.infer_known_record_access(record, label, location) } From b9052949f7170c21dc0e7692a466d55a024d2f87 Mon Sep 17 00:00:00 2001 From: KtorZ Date: Sat, 15 Mar 2025 21:11:46 +0100 Subject: [PATCH 06/16] hide internal functions from suggested hints on unknown modules/constructor/value. Signed-off-by: KtorZ --- crates/aiken-lang/Cargo.toml | 2 +- crates/aiken-lang/src/builtins.rs | 11 +++++++++++ crates/aiken-lang/src/tipo/environment.rs | 10 +++++++--- 3 files changed, 19 insertions(+), 4 deletions(-) diff --git a/crates/aiken-lang/Cargo.toml b/crates/aiken-lang/Cargo.toml index a443df3c..5b2ccafa 100644 --- a/crates/aiken-lang/Cargo.toml +++ b/crates/aiken-lang/Cargo.toml @@ -11,7 +11,7 @@ authors = [ "Kasey White ", "KtorZ ", ] -rust-version = "1.70.0" +rust-version = "1.80.0" build = "build.rs" [dependencies] diff --git a/crates/aiken-lang/src/builtins.rs b/crates/aiken-lang/src/builtins.rs index 9419ee83..774081e8 100644 --- a/crates/aiken-lang/src/builtins.rs +++ b/crates/aiken-lang/src/builtins.rs @@ -12,6 +12,7 @@ use crate::{ }, IdGenerator, }; +use std::{collections::BTreeSet, sync::LazyLock}; use indexmap::IndexMap; use std::{collections::HashMap, rc::Rc}; @@ -25,6 +26,16 @@ use uplc::{ pub const PRELUDE: &str = "aiken"; pub const BUILTIN: &str = "aiken/builtin"; +pub static INTERNAL_FUNCTIONS: LazyLock> = LazyLock::new(|| { + let mut set = BTreeSet::new(); + set.insert("diagnostic"); + set.insert("do_from_int"); + set.insert("encode_base16"); + set.insert("enumerate"); + set.insert("from_int"); + set +}); + /// Build a prelude that can be injected /// into a compiler pipeline pub fn prelude(id_gen: &IdGenerator) -> TypeInfo { diff --git a/crates/aiken-lang/src/tipo/environment.rs b/crates/aiken-lang/src/tipo/environment.rs index 6473ce91..3a2cd2f2 100644 --- a/crates/aiken-lang/src/tipo/environment.rs +++ b/crates/aiken-lang/src/tipo/environment.rs @@ -796,8 +796,7 @@ impl<'a> Environment<'a> { self.module_types .keys() .filter_map(|t| { - // Avoid leaking special internal types such as __ScriptContext or - // __ScriptPurpose. + // Avoid leaking special internal types in error hints. if t.starts_with("__") { None } else { @@ -811,6 +810,9 @@ impl<'a> Environment<'a> { self.scope .keys() .filter(|&t| PIPE_VARIABLE != t) + // Avoid leaking internal functions in error hints. + .filter(|&t| !crate::builtins::INTERNAL_FUNCTIONS.contains(t.as_str())) + .filter(|&t| !t.starts_with("__") && !TypeConstructor::might_be(t)) .map(|t| t.to_string()) .collect() } @@ -818,7 +820,9 @@ impl<'a> Environment<'a> { pub fn local_constructor_names(&self) -> Vec { self.scope .keys() - .filter(|&t| t.chars().next().unwrap_or_default().is_uppercase()) + // Avoid leaking internal constructors in error hints. + .filter(|&t| !t.starts_with("__")) + .filter(|&t| TypeConstructor::might_be(t)) .map(|t| t.to_string()) .collect() } From 660ff7fa314fe39aeeb20eafe4d6785f5217118c Mon Sep 17 00:00:00 2001 From: KtorZ Date: Sat, 15 Mar 2025 22:28:49 +0100 Subject: [PATCH 07/16] adjust spans & tweak errors for maximum UX Signed-off-by: KtorZ --- crates/aiken-lang/src/tipo/environment.rs | 8 +++++--- crates/aiken-lang/src/tipo/error.rs | 16 ++++++++++------ crates/aiken-lang/src/tipo/expr.rs | 22 +++++++++++++++++++--- crates/aiken-project/src/error.rs | 2 +- 4 files changed, 35 insertions(+), 13 deletions(-) diff --git a/crates/aiken-lang/src/tipo/environment.rs b/crates/aiken-lang/src/tipo/environment.rs index 3a2cd2f2..ab5fce6c 100644 --- a/crates/aiken-lang/src/tipo/environment.rs +++ b/crates/aiken-lang/src/tipo/environment.rs @@ -505,8 +505,10 @@ impl<'a> Environment<'a> { }), Some(Namespace::Type(t)) => { + let type_location = location.map(|start, _| (start, start + t.len())); + let parent_type = self.module_types.get(t).ok_or_else(|| Error::UnknownType { - location, + location: type_location, name: t.to_string(), types: self.known_type_names(), })?; @@ -514,8 +516,8 @@ impl<'a> Environment<'a> { self.unused_modules.remove(&parent_type.module); self.get_fully_qualified_value_constructor( - (parent_type.module.as_str(), location), - (t, location), + (parent_type.module.as_str(), type_location), + (t, type_location), (name, location.map(|start, end| (start + t.len() + 1, end))), ) } diff --git a/crates/aiken-lang/src/tipo/error.rs b/crates/aiken-lang/src/tipo/error.rs index 769b05df..dd7aea8d 100644 --- a/crates/aiken-lang/src/tipo/error.rs +++ b/crates/aiken-lang/src/tipo/error.rs @@ -808,7 +808,7 @@ Perhaps, try the following: }, #[error( - "I looked for '{}' in '{}' but couldn't find it.\n", + "I looked for '{}' in module '{}' but couldn't find it.\n", name.if_supports_color(Stdout, |s| s.purple()), module_name.if_supports_color(Stdout, |s| s.purple()) )] @@ -822,7 +822,7 @@ Perhaps, try the following: ) ))] UnknownModuleType { - #[label("unknown import")] + #[label("not exported?")] location: Span, name: String, module_name: String, @@ -1529,14 +1529,15 @@ fn suggest_import_constructor() -> String { ┍━ aiken/pet.ak ━ ==> ┍━ foo.ak ━━━━━━━━━━━━━━━━ │ {keyword_pub} {keyword_type} {type_Pet} {{ │ {keyword_use} aiken/pet.{{{type_Pet}, {variant_Dog}}} │ {variant_Cat} │ - │ {variant_Dog} │ {keyword_fn} foo(pet : {type_Pet}) {{ - │ }} │ {keyword_when} pet {keyword_is} {{ - │ pet.{variant_Cat} -> // ... + │ {variant_Dog} │ {keyword_fn} foo(pet: {type_Pet}) {{ + │ {variant_Fox} │ {keyword_when} pet {keyword_is} {{ + │ }} │ pet.{variant_Cat} -> // ... │ {variant_Dog} -> // ... + │ {type_Pet}.{variant_Fox} -> // ... │ }} │ }} - You must import its constructors explicitly to use them, or prefix them with the module's name. + You must import its constructors explicitly to use them, or prefix them with the module or type's name. "# , keyword_fn = "fn".if_supports_color(Stdout, |s| s.yellow()) , keyword_is = "is".if_supports_color(Stdout, |s| s.yellow()) @@ -1553,6 +1554,9 @@ fn suggest_import_constructor() -> String { , variant_Dog = "Dog" .if_supports_color(Stdout, |s| s.bright_blue()) .if_supports_color(Stdout, |s| s.bold()) + , variant_Fox = "Fox" + .if_supports_color(Stdout, |s| s.bright_blue()) + .if_supports_color(Stdout, |s| s.bold()) } } diff --git a/crates/aiken-lang/src/tipo/expr.rs b/crates/aiken-lang/src/tipo/expr.rs index 483a9839..8fa90958 100644 --- a/crates/aiken-lang/src/tipo/expr.rs +++ b/crates/aiken-lang/src/tipo/expr.rs @@ -1078,7 +1078,10 @@ impl<'a, 'b> ExprTyper<'a, 'b> { } if TypeConstructor::might_be(type_name) => { return self.infer_type_constructor_access( (type_name, type_location), - (&label, access_location), + ( + &label, + access_location.map(|start, end| (start + type_name.len() + 1, end)), + ), ); } @@ -1094,8 +1097,16 @@ impl<'a, 'b> ExprTyper<'a, 'b> { { return self.infer_inner_type_constructor_access( (module_name, *module_location), - (type_name, type_location), - (&label, access_location), + ( + type_name, + type_location.map(|start, end| (start + module_name.len() + 1, end)), + ), + ( + &label, + access_location.map(|start, end| { + (start + module_name.len() + type_name.len() + 2, end) + }), + ), ); } } @@ -1203,6 +1214,8 @@ impl<'a, 'b> ExprTyper<'a, 'b> { (type_name, type_location): (&str, Span), (label, label_location): (&str, Span), ) -> Result { + self.environment.increment_usage(type_name); + let parent_type = self .environment .module_types @@ -1234,6 +1247,9 @@ impl<'a, 'b> ExprTyper<'a, 'b> { (type_name, type_location), (label, label_location), )?; + + self.environment.unused_modules.remove(module_name); + self.infer_module_access( module_name, label.to_string(), diff --git a/crates/aiken-project/src/error.rs b/crates/aiken-project/src/error.rs index b5048195..0180e831 100644 --- a/crates/aiken-project/src/error.rs +++ b/crates/aiken-project/src/error.rs @@ -731,7 +731,7 @@ impl Warning { impl Debug for Warning { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - default_miette_handler(0) + default_miette_handler(1) .debug( &DisplayWarning { title: &self.to_string(), From 983902fca8f1b78358a8fd5918d95ffaabebb324 Mon Sep 17 00:00:00 2001 From: KtorZ Date: Sat, 15 Mar 2025 23:00:55 +0100 Subject: [PATCH 08/16] add extra regression test from stdlib. Signed-off-by: KtorZ --- crates/aiken-lang/src/tests/check.rs | 36 ++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/crates/aiken-lang/src/tests/check.rs b/crates/aiken-lang/src/tests/check.rs index c8e67fda..1e4afa1b 100644 --- a/crates/aiken-lang/src/tests/check.rs +++ b/crates/aiken-lang/src/tests/check.rs @@ -2267,6 +2267,42 @@ fn allow_expect_into_opaque_type_constructor_without_typecasting_in_module() { assert!(check(parse(source_code)).is_ok()); } +#[test] +fn use_imported_type_as_namespace_for_patterns() { + let dependency = r#" + pub type Credential { + VerificationKey(ByteArray) + Script(ByteArray) + } + "#; + + let source_code = r#" + use cardano/address.{Credential, Script, VerificationKey} + + pub fn compare(left: Credential, right: Credential) -> Ordering { + when left is { + Script(left) -> + when right is { + Script(right) -> Equal + _ -> Less + } + VerificationKey(left) -> + when right is { + Script(_) -> Greater + VerificationKey(right) -> Equal + } + } + } + "#; + + let result = check_with_deps( + dbg!(parse(source_code)), + vec![(parse_as(dependency, "cardano/address"))], + ); + + assert!(matches!(result, Ok(..)), "{result:#?}"); +} + #[test] fn use_type_as_namespace_for_patterns() { let dependency = r#" From 2adc1fab664232bf044b3e920f1ddd0a6ed926aa Mon Sep 17 00:00:00 2001 From: KtorZ Date: Sat, 15 Mar 2025 23:50:51 +0100 Subject: [PATCH 09/16] ensure import resolution is done according to local bindings Might it be from a module that has multiple path fragments or one that is aliased. Signed-off-by: KtorZ --- crates/aiken-lang/src/tests/check.rs | 75 ++++++++++++++++++++++- crates/aiken-lang/src/tipo/environment.rs | 29 ++++++++- crates/aiken-lang/src/tipo/expr.rs | 23 ++++++- 3 files changed, 122 insertions(+), 5 deletions(-) diff --git a/crates/aiken-lang/src/tests/check.rs b/crates/aiken-lang/src/tests/check.rs index 1e4afa1b..790b625d 100644 --- a/crates/aiken-lang/src/tests/check.rs +++ b/crates/aiken-lang/src/tests/check.rs @@ -2296,7 +2296,7 @@ fn use_imported_type_as_namespace_for_patterns() { "#; let result = check_with_deps( - dbg!(parse(source_code)), + parse(source_code), vec![(parse_as(dependency, "cardano/address"))], ); @@ -2443,6 +2443,52 @@ fn use_type_as_nested_namespace_for_constructors() { assert!(matches!(result, Ok(..)), "{result:#?}"); } +#[test] +fn use_type_as_nested_namespace_for_constructors_from_multi_level_module() { + let dependency = r#" + pub type Foo { + I(Int) + B(Bool) + } + "#; + + let source_code = r#" + use foo/bar + + test my_test() { + trace bar.Foo.I(42) + Void + } + "#; + + let result = check_with_deps(parse(source_code), vec![(parse_as(dependency, "foo/bar"))]); + + assert!(matches!(result, Ok(..)), "{result:#?}"); +} + +#[test] +fn use_type_as_nested_namespace_for_constructors_from_module_alias() { + let dependency = r#" + pub type Foo { + I(Int) + B(Bool) + } + "#; + + let source_code = r#" + use foo as bar + + test my_test() { + trace bar.Foo.I(42) + Void + } + "#; + + let result = check_with_deps(parse(source_code), vec![(parse_as(dependency, "foo"))]); + + assert!(matches!(result, Ok(..)), "{result:#?}"); +} + #[test] fn use_type_as_namespace_unknown_constructor() { let dependency = r#" @@ -2626,6 +2672,33 @@ fn use_opaque_type_as_nested_namespace_for_constructors_fails() { ); } +#[test] +fn use_non_imported_module_as_namespace() { + let dependency = r#" + pub type Foo { + I(Int) + B(Bool) + } + "#; + + let source_code = r#" + test my_test() { + trace foo.Foo.I(14) + Void + } + "#; + + let result = check_with_deps(parse(source_code), vec![(parse_as(dependency, "foo"))]); + + assert!( + matches!( + &result, + Err((warnings, Error::UnknownModule { name, .. })) if name == "foo" && warnings.is_empty(), + ), + "{result:#?}" + ); +} + #[test] fn forbid_importing_or_using_opaque_constructors() { let dependency = r#" diff --git a/crates/aiken-lang/src/tipo/environment.rs b/crates/aiken-lang/src/tipo/environment.rs index ab5fce6c..e814c45f 100644 --- a/crates/aiken-lang/src/tipo/environment.rs +++ b/crates/aiken-lang/src/tipo/environment.rs @@ -372,6 +372,31 @@ impl<'a> Environment<'a> { } } + /// Get an imported module's actual name in the current module, from its fully qualified module + /// name. + #[allow(clippy::result_large_err)] + pub fn local_module_name(&self, name: &str, location: Span) -> Result { + self.imported_modules + .iter() + .filter_map(|(k, module)| { + if module.1.name == name { + Some(k.to_string()) + } else { + None + } + }) + .next() + .ok_or_else(|| Error::UnknownModule { + location, + name: name.to_string(), + known_modules: self + .importable_modules + .keys() + .map(|t| t.to_string()) + .collect(), + }) + } + #[allow(clippy::result_large_err)] pub fn get_fully_qualified_value_constructor( &self, @@ -379,8 +404,8 @@ impl<'a> Environment<'a> { (type_name, type_location): (&str, Span), (value, value_location): (&str, Span), ) -> Result<&ValueConstructor, Error> { - let (_, module) = - self.imported_modules + let module = + self.importable_modules .get(module_name) .ok_or_else(|| Error::UnknownModule { location: module_location, diff --git a/crates/aiken-lang/src/tipo/expr.rs b/crates/aiken-lang/src/tipo/expr.rs index 8fa90958..c6a289ec 100644 --- a/crates/aiken-lang/src/tipo/expr.rs +++ b/crates/aiken-lang/src/tipo/expr.rs @@ -1095,8 +1095,25 @@ impl<'a, 'b> ExprTyper<'a, 'b> { location: module_location, } = type_container.as_ref() { + // Lookup the module using the declared name (which may have been rebind with + // 'as'), to obtain its _full unambiguous name_. + let (_, module) = self + .environment + .imported_modules + .get(module_name) + .ok_or_else(|| Error::UnknownModule { + location: *module_location, + name: module_name.to_string(), + known_modules: self + .environment + .importable_modules + .keys() + .map(|t| t.to_string()) + .collect(), + })?; + return self.infer_inner_type_constructor_access( - (module_name, *module_location), + (module.name.as_str(), *module_location), ( type_name, type_location.map(|start, end| (start + module_name.len() + 1, end)), @@ -1251,7 +1268,9 @@ impl<'a, 'b> ExprTyper<'a, 'b> { self.environment.unused_modules.remove(module_name); self.infer_module_access( - module_name, + &self + .environment + .local_module_name(module_name, module_location)?, label.to_string(), &type_location, label_location, From b8f42dd5556a5d8b3d154f1e90106bb82cc89f7d Mon Sep 17 00:00:00 2001 From: KtorZ Date: Sun, 16 Mar 2025 00:26:44 +0100 Subject: [PATCH 10/16] also support using Types as namespace when nested in module. Signed-off-by: KtorZ --- crates/aiken-lang/src/ast.rs | 2 +- crates/aiken-lang/src/format.rs | 8 +++- .../src/parser/pattern/constructor.rs | 23 ++++++++++- crates/aiken-lang/src/parser/pattern/mod.rs | 2 +- .../snapshots/constructor_type_select.snap | 1 + crates/aiken-lang/src/tests/check.rs | 25 ++++++++++++ crates/aiken-lang/src/tipo/environment.rs | 39 ++++++++++++++++++- crates/aiken-lang/src/tipo/pattern.rs | 4 +- 8 files changed, 96 insertions(+), 8 deletions(-) diff --git a/crates/aiken-lang/src/ast.rs b/crates/aiken-lang/src/ast.rs index 6283737a..d3882d51 100644 --- a/crates/aiken-lang/src/ast.rs +++ b/crates/aiken-lang/src/ast.rs @@ -1656,7 +1656,7 @@ impl TypedPattern { #[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)] pub enum Namespace { Module(String), - Type(String), + Type(Option, String), } #[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)] diff --git a/crates/aiken-lang/src/format.rs b/crates/aiken-lang/src/format.rs index 64d7c876..09b184a8 100644 --- a/crates/aiken-lang/src/format.rs +++ b/crates/aiken-lang/src/format.rs @@ -1217,9 +1217,15 @@ impl<'comments> Formatter<'comments> { } let name = match module { - Some(Namespace::Module(m)) | Some(Namespace::Type(m)) => { + Some(Namespace::Module(m)) | Some(Namespace::Type(None, m)) => { m.to_doc().append(".").append(name) } + Some(Namespace::Type(Some(m), c)) => m + .to_doc() + .append(".") + .append(c.as_str()) + .append(".") + .append(name), None => name.to_doc(), }; diff --git a/crates/aiken-lang/src/parser/pattern/constructor.rs b/crates/aiken-lang/src/parser/pattern/constructor.rs index ebfa681e..d3ddfe9e 100644 --- a/crates/aiken-lang/src/parser/pattern/constructor.rs +++ b/crates/aiken-lang/src/parser/pattern/constructor.rs @@ -9,6 +9,27 @@ pub fn parser( pattern: Recursive<'_, Token, UntypedPattern, ParseError>, ) -> impl Parser + '_ { choice(( + select! { Token::Name { name } => name } + .then(just(Token::Dot).ignore_then(select! {Token::UpName { name } => name})) + .then( + just(Token::Dot).ignore_then( + select! {Token::UpName { name } => name}.then(args(pattern.clone())), + ), + ) + .map_with_span( + |((module, namespace), (name, (arguments, spread_location, is_record))), span| { + UntypedPattern::Constructor { + is_record, + location: span, + name, + arguments, + module: Some(Namespace::Type(Some(module), namespace)), + constructor: (), + spread_location, + tipo: (), + } + }, + ), select! { Token::UpName { name } => name } .then( just(Token::Dot).ignore_then( @@ -22,7 +43,7 @@ pub fn parser( location: span, name, arguments, - module: Some(Namespace::Type(namespace)), + module: Some(Namespace::Type(None, namespace)), constructor: (), spread_location, tipo: (), diff --git a/crates/aiken-lang/src/parser/pattern/mod.rs b/crates/aiken-lang/src/parser/pattern/mod.rs index 97c6be58..e0c64cfd 100644 --- a/crates/aiken-lang/src/parser/pattern/mod.rs +++ b/crates/aiken-lang/src/parser/pattern/mod.rs @@ -27,9 +27,9 @@ pub use var::parser as var; pub fn parser() -> impl Parser { recursive(|pattern| { choice(( - var(pattern.clone()), pair(pattern.clone()), constructor(pattern.clone()), + var(pattern.clone()), discard(), int(), bytearray(), diff --git a/crates/aiken-lang/src/parser/pattern/snapshots/constructor_type_select.snap b/crates/aiken-lang/src/parser/pattern/snapshots/constructor_type_select.snap index 468e6816..8eb4d3cb 100644 --- a/crates/aiken-lang/src/parser/pattern/snapshots/constructor_type_select.snap +++ b/crates/aiken-lang/src/parser/pattern/snapshots/constructor_type_select.snap @@ -9,6 +9,7 @@ Constructor { arguments: [], module: Some( Type( + None, "Foo", ), ), diff --git a/crates/aiken-lang/src/tests/check.rs b/crates/aiken-lang/src/tests/check.rs index 790b625d..77017615 100644 --- a/crates/aiken-lang/src/tests/check.rs +++ b/crates/aiken-lang/src/tests/check.rs @@ -2328,6 +2328,31 @@ fn use_type_as_namespace_for_patterns() { assert!(matches!(result, Ok(..)), "{result:#?}"); } +#[test] +fn use_nested_type_as_namespace_for_patterns() { + let dependency = r#" + pub type Foo { + A + B + } + "#; + + let source_code = r#" + use foo.{Foo} + + fn bar(x: Foo) { + when x is { + foo.Foo.A -> True + foo.Foo.B -> False + } + } + "#; + + let result = check_with_deps(parse(source_code), vec![(parse_as(dependency, "foo"))]); + + assert!(matches!(result, Ok(..)), "{result:#?}"); +} + #[test] fn use_opaque_type_as_namespace_for_patterns_fails() { let dependency = r#" diff --git a/crates/aiken-lang/src/tipo/environment.rs b/crates/aiken-lang/src/tipo/environment.rs index e814c45f..dafa622a 100644 --- a/crates/aiken-lang/src/tipo/environment.rs +++ b/crates/aiken-lang/src/tipo/environment.rs @@ -529,7 +529,42 @@ impl<'a> Environment<'a> { constructors: self.local_constructor_names(), }), - Some(Namespace::Type(t)) => { + Some(Namespace::Type(Some(module_name), t)) => { + let module_location = location.map(|start, _| (start, start + module_name.len())); + + // Lookup the module using the declared name (which may have been rebind with + // 'as'), to obtain its _full unambiguous name_. + let (_, module) = + self.imported_modules + .get(module_name) + .ok_or_else(|| Error::UnknownModule { + location: module_location, + name: module_name.to_string(), + known_modules: self + .importable_modules + .keys() + .map(|t| t.to_string()) + .collect(), + })?; + + let type_location = Span::create(module_location.end + 1, t.len()); + + let parent_type = module.types.get(t).ok_or_else(|| Error::UnknownType { + location: type_location, + name: t.to_string(), + types: self.known_type_names(), + })?; + + self.unused_modules.remove(&parent_type.module); + + self.get_fully_qualified_value_constructor( + (parent_type.module.as_str(), module_location), + (t, type_location), + (name, location.map(|_, end| (type_location.end + 1, end))), + ) + } + + Some(Namespace::Type(None, t)) => { let type_location = location.map(|start, _| (start, start + t.len())); let parent_type = self.module_types.get(t).ok_or_else(|| Error::UnknownType { @@ -558,7 +593,7 @@ impl<'a> Environment<'a> { .keys() .map(|t| t.to_string()) .collect(), - location, + location: Span::create(location.start, m.len()), })?; self.unused_modules.remove(m); diff --git a/crates/aiken-lang/src/tipo/pattern.rs b/crates/aiken-lang/src/tipo/pattern.rs index d3a23afc..b9dc0b1c 100644 --- a/crates/aiken-lang/src/tipo/pattern.rs +++ b/crates/aiken-lang/src/tipo/pattern.rs @@ -573,7 +573,7 @@ impl<'a, 'b> PatternTyper<'a, 'b> { // NOTE: // Type namespaces are completely erased during type-check. module: match module { - None | Some(Namespace::Type(_)) => None, + None | Some(Namespace::Type(..)) => None, Some(Namespace::Module(m)) => Some(m), }, name, @@ -609,7 +609,7 @@ impl<'a, 'b> PatternTyper<'a, 'b> { // NOTE: // Type namespaces are completely erased during type-check. module: match module { - None | Some(Namespace::Type(_)) => None, + None | Some(Namespace::Type(..)) => None, Some(Namespace::Module(m)) => Some(m), }, name, From 18d2beeadbab415b04ec9f0e35499c3cd9d8d68c Mon Sep 17 00:00:00 2001 From: KtorZ Date: Sun, 16 Mar 2025 00:29:39 +0100 Subject: [PATCH 11/16] add format test Signed-off-by: KtorZ --- crates/aiken-lang/src/tests/format.rs | 23 +++++++++++++++++++ .../tests/snapshots/types_as_namespace.snap | 19 +++++++++++++++ 2 files changed, 42 insertions(+) create mode 100644 crates/aiken-lang/src/tests/snapshots/types_as_namespace.snap diff --git a/crates/aiken-lang/src/tests/format.rs b/crates/aiken-lang/src/tests/format.rs index 2c43ba43..0c4c1c65 100644 --- a/crates/aiken-lang/src/tests/format.rs +++ b/crates/aiken-lang/src/tests/format.rs @@ -1449,3 +1449,26 @@ fn capture_right_hand_side_assign() { "# ); } + +#[test] +fn types_as_namespace() { + assert_format!( + r#" + use foo.{ Foo } + + fn predicate(val) { + when val is { + Foo.I(n) -> n >= 14 + foo.Foo.B(bytes) -> bytes == "aiken" + } + } + + test my_test() { + and { + predicate(foo.Foo.I(42)), + predicate(Foo.b("aiken")) + } + } + "# + ); +} diff --git a/crates/aiken-lang/src/tests/snapshots/types_as_namespace.snap b/crates/aiken-lang/src/tests/snapshots/types_as_namespace.snap new file mode 100644 index 00000000..9f8d6559 --- /dev/null +++ b/crates/aiken-lang/src/tests/snapshots/types_as_namespace.snap @@ -0,0 +1,19 @@ +--- +source: crates/aiken-lang/src/tests/format.rs +description: "Code:\n\nuse foo.{ Foo }\n\nfn predicate(val) {\n when val is {\n Foo.I(n) -> n >= 14\n foo.Foo.B(bytes) -> bytes == \"aiken\"\n }\n}\n\ntest my_test() {\n and {\n predicate(foo.Foo.I(42)),\n predicate(Foo.b(\"aiken\"))\n }\n}\n" +--- +use foo.{Foo} + +fn predicate(val) { + when val is { + Foo.I(n) -> n >= 14 + foo.Foo.B(bytes) -> bytes == "aiken" + } +} + +test my_test() { + and { + predicate(foo.Foo.I(42)), + predicate(Foo.b("aiken")), + } +} From d7af418a63e61504444880453e4ff089840d7968 Mon Sep 17 00:00:00 2001 From: KtorZ Date: Sun, 16 Mar 2025 00:45:20 +0100 Subject: [PATCH 12/16] return dedicated error on invalid type field access, instead of confusing 'unknown module.' Signed-off-by: KtorZ --- crates/aiken-lang/src/tests/check.rs | 58 ++++++++++++++++++++++++++++ crates/aiken-lang/src/tipo/error.rs | 17 ++++++-- crates/aiken-lang/src/tipo/expr.rs | 6 +++ 3 files changed, 78 insertions(+), 3 deletions(-) diff --git a/crates/aiken-lang/src/tests/check.rs b/crates/aiken-lang/src/tests/check.rs index 77017615..05753ef3 100644 --- a/crates/aiken-lang/src/tests/check.rs +++ b/crates/aiken-lang/src/tests/check.rs @@ -2724,6 +2724,64 @@ fn use_non_imported_module_as_namespace() { ); } +#[test] +fn invalid_type_field_access_chain() { + let dependency = r#" + pub type Foo { + I(Int) + B(Bool) + } + "#; + + let source_code = r#" + use foo.{Foo} + + test my_test() { + trace Foo.I.Int(42) + Void + } + "#; + + let result = check_with_deps(parse(source_code), vec![(parse_as(dependency, "foo"))]); + + assert!( + matches!( + &result, + Err((warnings, Error::InvalidFieldAccess { .. })) if warnings.is_empty(), + ), + "{result:#?}" + ); +} + +#[test] +fn invalid_type_field_access_chain_2() { + let dependency = r#" + pub type Foo { + I(Int) + B(Bool) + } + "#; + + let source_code = r#" + use foo.{Foo} + + test my_test() { + trace Foo.i(42) + Void + } + "#; + + let result = check_with_deps(parse(source_code), vec![(parse_as(dependency, "foo"))]); + + assert!( + matches!( + &result, + Err((warnings, Error::UnknownTypeConstructor { .. })) if warnings.is_empty(), + ), + "{result:#?}" + ); +} + #[test] fn forbid_importing_or_using_opaque_constructors() { let dependency = r#" diff --git a/crates/aiken-lang/src/tipo/error.rs b/crates/aiken-lang/src/tipo/error.rs index dd7aea8d..2a438f4a 100644 --- a/crates/aiken-lang/src/tipo/error.rs +++ b/crates/aiken-lang/src/tipo/error.rs @@ -1079,7 +1079,7 @@ The best thing to do from here is to remove it."#))] available_purposes: Vec, }, - #[error("I could not find an appropriate handler in the validator definition\n")] + #[error("I could not find an appropriate handler in the validator definition.\n")] #[diagnostic(code("unknown::handler"))] #[diagnostic(help( "When referring to a validator handler via record access, you must refer to one of the declared handlers{}{}", @@ -1095,7 +1095,7 @@ The best thing to do from here is to remove it."#))] available_handlers: Vec, }, - #[error("I caught an extraneous fallback handler in an already exhaustive validator\n")] + #[error("I caught an extraneous fallback handler in an already exhaustive validator.\n")] #[diagnostic(code("extraneous::fallback"))] #[diagnostic(help( "Validator handlers must be exhaustive and either cover all purposes, or provide a fallback handler. Here, you have successfully covered all script purposes with your handler, but left an extraneous fallback branch. I cannot let that happen, but removing it for you would probably be deemed rude. So please, remove the fallback." @@ -1104,6 +1104,16 @@ The best thing to do from here is to remove it."#))] #[label("redundant fallback handler")] fallback: Span, }, + + #[error("I was stopped by a suspicious field access chain.\n")] + #[diagnostic(code("invalid::field_access"))] + #[diagnostic(help( + "It seems like you've got things mixed up a little here? You can only access fields exported by modules or, by types within those modules. Double-check the culprit field access chain, there's likely something wrong about it." + ))] + InvalidFieldAccess { + #[label("invalid field access")] + location: Span, + }, } impl ExtraData for Error { @@ -1166,7 +1176,8 @@ impl ExtraData for Error { | Error::UnknownValidatorHandler { .. } | Error::UnexpectedValidatorFallback { .. } | Error::IncorrectBenchmarkArity { .. } - | Error::MustInferFirst { .. } => None, + | Error::MustInferFirst { .. } + | Error::InvalidFieldAccess { .. } => None, Error::UnknownType { name, .. } | Error::UnknownTypeConstructor { name, .. } diff --git a/crates/aiken-lang/src/tipo/expr.rs b/crates/aiken-lang/src/tipo/expr.rs index c6a289ec..080b3195 100644 --- a/crates/aiken-lang/src/tipo/expr.rs +++ b/crates/aiken-lang/src/tipo/expr.rs @@ -1095,6 +1095,12 @@ impl<'a, 'b> ExprTyper<'a, 'b> { location: module_location, } = type_container.as_ref() { + if TypeConstructor::might_be(module_name) { + return Err(Error::InvalidFieldAccess { + location: access_location, + }); + } + // Lookup the module using the declared name (which may have been rebind with // 'as'), to obtain its _full unambiguous name_. let (_, module) = self From 9d05011001d50f0569db4b5ff406811cafbdab57 Mon Sep 17 00:00:00 2001 From: KtorZ Date: Sun, 16 Mar 2025 00:58:50 +0100 Subject: [PATCH 13/16] factor out common logic for creating UnknownModule error in a cute little helper. Signed-off-by: KtorZ --- crates/aiken-lang/src/tipo/environment.rs | 101 +++++++--------------- crates/aiken-lang/src/tipo/expr.rs | 36 ++------ 2 files changed, 39 insertions(+), 98 deletions(-) diff --git a/crates/aiken-lang/src/tipo/environment.rs b/crates/aiken-lang/src/tipo/environment.rs index dafa622a..224bb5e1 100644 --- a/crates/aiken-lang/src/tipo/environment.rs +++ b/crates/aiken-lang/src/tipo/environment.rs @@ -91,6 +91,19 @@ pub struct Environment<'a> { } impl<'a> Environment<'a> { + #[allow(clippy::result_large_err)] + pub fn err_unknown_module(&self, name: String, location: Span) -> Error { + Error::UnknownModule { + name, + location, + known_modules: self + .importable_modules + .keys() + .map(|t| t.to_string()) + .collect(), + } + } + #[allow(clippy::result_large_err)] pub fn find_module(&self, fragments: &[String], location: Span) -> Result<&'a TypeInfo, Error> { let mut name = fragments.join("/"); @@ -118,11 +131,7 @@ impl<'a> Environment<'a> { .collect(), } } else { - Error::UnknownModule { - location, - name, - known_modules: self.importable_modules.keys().cloned().collect(), - } + self.err_unknown_module(name, location) } }) } @@ -386,15 +395,7 @@ impl<'a> Environment<'a> { } }) .next() - .ok_or_else(|| Error::UnknownModule { - location, - name: name.to_string(), - known_modules: self - .importable_modules - .keys() - .map(|t| t.to_string()) - .collect(), - }) + .ok_or_else(|| self.err_unknown_module(name.to_string(), location)) } #[allow(clippy::result_large_err)] @@ -404,18 +405,10 @@ impl<'a> Environment<'a> { (type_name, type_location): (&str, Span), (value, value_location): (&str, Span), ) -> Result<&ValueConstructor, Error> { - let module = - self.importable_modules - .get(module_name) - .ok_or_else(|| Error::UnknownModule { - location: module_location, - name: module_name.to_string(), - known_modules: self - .importable_modules - .keys() - .map(|t| t.to_string()) - .collect(), - })?; + let module = self + .importable_modules + .get(module_name) + .ok_or_else(|| self.err_unknown_module(module_name.to_string(), module_location))?; let constructors = module @@ -483,18 +476,10 @@ impl<'a> Environment<'a> { }), Some(m) => { - let (_, module) = - self.imported_modules - .get(m) - .ok_or_else(|| Error::UnknownModule { - location, - name: name.to_string(), - known_modules: self - .importable_modules - .keys() - .map(|t| t.to_string()) - .collect(), - })?; + let (_, module) = self + .imported_modules + .get(m) + .ok_or_else(|| self.err_unknown_module(name.to_string(), location))?; self.unused_modules.remove(m); @@ -534,18 +519,9 @@ impl<'a> Environment<'a> { // Lookup the module using the declared name (which may have been rebind with // 'as'), to obtain its _full unambiguous name_. - let (_, module) = - self.imported_modules - .get(module_name) - .ok_or_else(|| Error::UnknownModule { - location: module_location, - name: module_name.to_string(), - known_modules: self - .importable_modules - .keys() - .map(|t| t.to_string()) - .collect(), - })?; + let (_, module) = self.imported_modules.get(module_name).ok_or_else(|| { + self.err_unknown_module(module_name.to_string(), module_location) + })?; let type_location = Span::create(module_location.end + 1, t.len()); @@ -583,18 +559,9 @@ impl<'a> Environment<'a> { } Some(Namespace::Module(m)) => { - let (_, module) = - self.imported_modules - .get(m) - .ok_or_else(|| Error::UnknownModule { - name: m.to_string(), - known_modules: self - .importable_modules - .keys() - .map(|t| t.to_string()) - .collect(), - location: Span::create(location.start, m.len()), - })?; + let (_, module) = self.imported_modules.get(m).ok_or_else(|| { + self.err_unknown_module(m.to_string(), Span::create(location.start, m.len())) + })?; self.unused_modules.remove(m); @@ -1985,15 +1952,7 @@ impl<'a> Environment<'a> { let module = self .importable_modules .get(full_module_name) - .ok_or_else(|| Error::UnknownModule { - location, - name: name.to_string(), - known_modules: self - .importable_modules - .keys() - .map(|t| t.to_string()) - .collect(), - })?; + .ok_or_else(|| self.err_unknown_module(name.to_string(), location))?; self.unused_modules.remove(full_module_name); diff --git a/crates/aiken-lang/src/tipo/expr.rs b/crates/aiken-lang/src/tipo/expr.rs index 080b3195..43fe0dd6 100644 --- a/crates/aiken-lang/src/tipo/expr.rs +++ b/crates/aiken-lang/src/tipo/expr.rs @@ -1107,15 +1107,9 @@ impl<'a, 'b> ExprTyper<'a, 'b> { .environment .imported_modules .get(module_name) - .ok_or_else(|| Error::UnknownModule { - location: *module_location, - name: module_name.to_string(), - known_modules: self - .environment - .importable_modules - .keys() - .map(|t| t.to_string()) - .collect(), + .ok_or_else(|| { + self.environment + .err_unknown_module(module_name.to_string(), *module_location) })?; return self.infer_inner_type_constructor_access( @@ -1175,15 +1169,9 @@ impl<'a, 'b> ExprTyper<'a, 'b> { .environment .imported_modules .get(module_alias) - .ok_or_else(|| Error::UnknownModule { - name: module_alias.to_string(), - location: *module_location, - known_modules: self - .environment - .importable_modules - .keys() - .map(|t| t.to_string()) - .collect(), + .ok_or_else(|| { + self.environment + .err_unknown_module(module_alias.to_string(), *module_location) })?; let constructor = @@ -2683,15 +2671,9 @@ impl<'a, 'b> ExprTyper<'a, 'b> { .environment .imported_modules .get(module_name) - .ok_or_else(|| Error::UnknownModule { - location: *location, - name: module_name.to_string(), - known_modules: self - .environment - .importable_modules - .keys() - .map(|t| t.to_string()) - .collect(), + .ok_or_else(|| { + self.environment + .err_unknown_module(module_name.to_string(), *location) })?; module From 0ed94c18d74ee08c34504a729b71c806c7c16715 Mon Sep 17 00:00:00 2001 From: KtorZ Date: Sun, 16 Mar 2025 01:11:28 +0100 Subject: [PATCH 14/16] fill-in CHANGELOG. Signed-off-by: KtorZ --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2660b872..4dd6af9a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ ### Added - **aiken**: New `-S` flag on `check` and `build` that blocks the printing of warnings but it still shows the total warning count. @rvcas +- **aiken-lang**: Allow types to be used as namespaces for constructors. Importing each constructor variants independently is no longer required in neither pattern-matches nor value construction. One can simply use the type name as a prefix/namespace now. @KtorZ ### Changed From 6a4a602391df0e2bd74811513afd6db2d1327704 Mon Sep 17 00:00:00 2001 From: KtorZ Date: Sun, 16 Mar 2025 13:17:05 +0100 Subject: [PATCH 15/16] have namespaces work also for calls to record-like constructors. Signed-off-by: KtorZ --- crates/aiken-lang/src/parser/chain/mod.rs | 1 + crates/aiken-lang/src/parser/expr/record.rs | 79 +++++++++++++++------ crates/aiken-lang/src/tests/check.rs | 55 +++++++------- 3 files changed, 87 insertions(+), 48 deletions(-) diff --git a/crates/aiken-lang/src/parser/chain/mod.rs b/crates/aiken-lang/src/parser/chain/mod.rs index c56ff929..a791c5ae 100644 --- a/crates/aiken-lang/src/parser/chain/mod.rs +++ b/crates/aiken-lang/src/parser/chain/mod.rs @@ -4,6 +4,7 @@ pub(crate) mod call; pub(crate) mod field_access; pub(crate) mod tuple_index; +#[derive(Debug)] pub(crate) enum Chain { Call(Vec, Span), FieldAccess(String, Span), diff --git a/crates/aiken-lang/src/parser/expr/record.rs b/crates/aiken-lang/src/parser/expr/record.rs index d947d7f7..2e654f69 100644 --- a/crates/aiken-lang/src/parser/expr/record.rs +++ b/crates/aiken-lang/src/parser/expr/record.rs @@ -16,6 +16,12 @@ pub fn parser( .map_with_span(|module, span: ast::Span| (module, span)) .then_ignore(just(Token::Dot)) .or_not() + .then( + select! {Token::UpName { name } => name} + .map_with_span(|name, span| (name, span)) + .then_ignore(just(Token::Dot)) + .or_not(), + ) .then(select! {Token::UpName { name } => name}.map_with_span(|name, span| (name, span))) .then( choice(( @@ -117,6 +123,12 @@ pub fn parser( .map_with_span(|module, span| (module, span)) .then_ignore(just(Token::Dot)) .or_not() + .then( + select! {Token::UpName { name } => name} + .map_with_span(|name, span| (name, span)) + .then_ignore(just(Token::Dot)) + .or_not(), + ) .then(select! {Token::UpName { name } => name}.map_with_span(|name, span| (name, span))) .then( select! {Token::Name {name} => name} @@ -157,29 +169,52 @@ pub fn parser( .delimited_by(just(Token::LeftParen), just(Token::RightParen)), ), )) - .map_with_span(|((module, (name, n_span)), arguments), span| { - let fun = if let Some((module, m_span)) = module { - UntypedExpr::FieldAccess { - location: m_span.union(n_span), - label: name, - container: Box::new(UntypedExpr::Var { - location: m_span, - name: module, - }), - } - } else { - UntypedExpr::Var { - location: n_span, - name, - } - }; + .map_with_span( + |(((module, namespace), (label, label_span)), arguments), span| { + let fun = match (module, namespace) { + (Some((module, module_span)), Some((namespace, namespace_span))) => { + UntypedExpr::FieldAccess { + location: module_span.union(namespace_span).union(label_span), + label, + container: Box::new(UntypedExpr::FieldAccess { + location: module_span.union(namespace_span), + label: namespace, + container: Box::new(UntypedExpr::Var { + location: module_span, + name: module, + }), + }), + } + } + (None, Some((namespace, namespace_span))) => UntypedExpr::FieldAccess { + location: namespace_span.union(label_span), + label, + container: Box::new(UntypedExpr::Var { + location: namespace_span, + name: namespace, + }), + }, + (Some((module, module_span)), None) => UntypedExpr::FieldAccess { + location: module_span.union(label_span), + label, + container: Box::new(UntypedExpr::Var { + location: module_span, + name: module, + }), + }, + (None, None) => UntypedExpr::Var { + location: label_span, + name: label, + }, + }; - UntypedExpr::Call { - arguments, - fun: Box::new(fun), - location: span, - } - }) + UntypedExpr::Call { + arguments, + fun: Box::new(fun), + location: span, + } + }, + ) } #[cfg(test)] diff --git a/crates/aiken-lang/src/tests/check.rs b/crates/aiken-lang/src/tests/check.rs index 05753ef3..557d0495 100644 --- a/crates/aiken-lang/src/tests/check.rs +++ b/crates/aiken-lang/src/tests/check.rs @@ -2307,7 +2307,7 @@ fn use_imported_type_as_namespace_for_patterns() { fn use_type_as_namespace_for_patterns() { let dependency = r#" pub type Foo { - A + A { a: Int } B } "#; @@ -2317,7 +2317,7 @@ fn use_type_as_namespace_for_patterns() { fn bar(foo: Foo) { when foo is { - Foo.A -> True + Foo.A { a } -> a > 10 Foo.B -> False } } @@ -2332,7 +2332,7 @@ fn use_type_as_namespace_for_patterns() { fn use_nested_type_as_namespace_for_patterns() { let dependency = r#" pub type Foo { - A + A { a: Int } B } "#; @@ -2342,7 +2342,7 @@ fn use_nested_type_as_namespace_for_patterns() { fn bar(x: Foo) { when x is { - foo.Foo.A -> True + foo.Foo.A { a } -> a > 10 foo.Foo.B -> False } } @@ -2357,7 +2357,7 @@ fn use_nested_type_as_namespace_for_patterns() { fn use_opaque_type_as_namespace_for_patterns_fails() { let dependency = r#" pub opaque type Foo { - A + A { a: Int } B } "#; @@ -2367,7 +2367,7 @@ fn use_opaque_type_as_namespace_for_patterns_fails() { fn bar(foo: Foo) { when foo is { - Foo.A -> True + Foo.A { a } -> a > 10 Foo.B -> False } } @@ -2388,7 +2388,7 @@ fn use_opaque_type_as_namespace_for_patterns_fails() { fn use_wrong_type_as_namespace_for_patterns_fails() { let dependency = r#" pub type Foo { - A + A { a: Int } B } @@ -2403,7 +2403,7 @@ fn use_wrong_type_as_namespace_for_patterns_fails() { fn bar(foo: Foo) { when foo is { - Foo.A -> True + Foo.A { .. } -> True Foo.D -> False } } @@ -2424,7 +2424,7 @@ fn use_wrong_type_as_namespace_for_patterns_fails() { fn use_type_as_namespace_for_constructors() { let dependency = r#" pub type Foo { - I(Int) + I { i: Int } B(Bool) } "#; @@ -2434,7 +2434,7 @@ fn use_type_as_namespace_for_constructors() { test my_test() { and { - Foo.I(42) == foo.I(42), + Foo.I { i: 42 } == foo.I(42), foo.B(True) == Foo.B(True), } } @@ -2449,7 +2449,7 @@ fn use_type_as_namespace_for_constructors() { fn use_type_as_nested_namespace_for_constructors() { let dependency = r#" pub type Foo { - I(Int) + I { i: Int } B(Bool) } "#; @@ -2458,7 +2458,8 @@ fn use_type_as_nested_namespace_for_constructors() { use foo test my_test() { - trace foo.Foo.I(42) + trace foo.Foo.I { i: 42 } + trace foo.Foo.B(False) Void } "#; @@ -2472,7 +2473,7 @@ fn use_type_as_nested_namespace_for_constructors() { fn use_type_as_nested_namespace_for_constructors_from_multi_level_module() { let dependency = r#" pub type Foo { - I(Int) + I { i: Int } B(Bool) } "#; @@ -2481,7 +2482,8 @@ fn use_type_as_nested_namespace_for_constructors_from_multi_level_module() { use foo/bar test my_test() { - trace bar.Foo.I(42) + trace bar.Foo.I { i: 42 } + trace bar.Foo.B(True) Void } "#; @@ -2495,7 +2497,7 @@ fn use_type_as_nested_namespace_for_constructors_from_multi_level_module() { fn use_type_as_nested_namespace_for_constructors_from_module_alias() { let dependency = r#" pub type Foo { - I(Int) + I { i: Int } B(Bool) } "#; @@ -2504,7 +2506,8 @@ fn use_type_as_nested_namespace_for_constructors_from_module_alias() { use foo as bar test my_test() { - trace bar.Foo.I(42) + trace bar.Foo.I { i: 42 } + trace bar.Foo.B(True) Void } "#; @@ -2518,7 +2521,7 @@ fn use_type_as_nested_namespace_for_constructors_from_module_alias() { fn use_type_as_namespace_unknown_constructor() { let dependency = r#" pub type Foo { - I(Int) + I { i: Int } B(Bool) } "#; @@ -2527,7 +2530,7 @@ fn use_type_as_namespace_unknown_constructor() { use foo.{Foo} test my_test() { - Foo.A == Foo.I(42) + Foo.I(42) == Foo.A } "#; @@ -2546,12 +2549,12 @@ fn use_type_as_namespace_unknown_constructor() { fn use_wrong_type_as_namespace() { let dependency = r#" pub type Foo { - I(Int) + I { i: Int } B(Bool) } pub type Bar { - S(String) + S { s: String } L(List) } "#; @@ -2580,12 +2583,12 @@ fn use_wrong_type_as_namespace() { fn use_wrong_nested_type_as_namespace() { let dependency = r#" pub type Foo { - I(Int) + I { i: Int } B(Bool) } pub type Bar { - S(String) + S { s: String } L(List) } "#; @@ -2614,7 +2617,7 @@ fn use_wrong_nested_type_as_namespace() { fn use_private_type_as_nested_namespace_fails() { let dependency = r#" type Foo { - I(Int) + I { i: Int } B(Bool) } "#; @@ -2623,7 +2626,7 @@ fn use_private_type_as_nested_namespace_fails() { use foo test my_test() { - trace foo.Foo.I(42) + trace foo.Foo.I { i: 42 } Void } "#; @@ -2643,7 +2646,7 @@ fn use_private_type_as_nested_namespace_fails() { fn use_opaque_type_as_namespace_for_constructors_fails() { let dependency = r#" pub opaque type Foo { - I(Int) + I { i: Int } B(Bool) } "#; @@ -2672,7 +2675,7 @@ fn use_opaque_type_as_namespace_for_constructors_fails() { fn use_opaque_type_as_nested_namespace_for_constructors_fails() { let dependency = r#" pub opaque type Foo { - I(Int) + I { i: Int } B(Bool) } "#; From 776aea86f548fdd4b089160b2cdc76b66b8c7190 Mon Sep 17 00:00:00 2001 From: KtorZ Date: Sun, 16 Mar 2025 14:07:25 +0100 Subject: [PATCH 16/16] Fix hint when suggesting to use named fields Wrongly suggesting args in lexicographical order instead of definition order... The tests were unfortunately only examplifying situations where the fields where also defined in lexicographical order... thus never really showing the issue. It came up in the real world, though. Whoopsie. Signed-off-by: KtorZ --- CHANGELOG.md | 1 + crates/aiken-lang/src/tests/check.rs | 53 +++++++++++++++++++++++++++ crates/aiken-lang/src/tipo/pattern.rs | 2 +- 3 files changed, 55 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2660b872..49f95421 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,7 @@ ### Fixed - **aiken-lang**: Formatter was removing comments from function type annotation args @rvcas +- **aiken-lang**: Fix hint when suggesting to use named fields, wrongly suggesting args in lexicographical order instead of definition order. @KtorZ ## v1.1.13 - 2025-02-26 diff --git a/crates/aiken-lang/src/tests/check.rs b/crates/aiken-lang/src/tests/check.rs index 9410b801..ba999e05 100644 --- a/crates/aiken-lang/src/tests/check.rs +++ b/crates/aiken-lang/src/tests/check.rs @@ -3701,3 +3701,56 @@ fn unused_record_fields_4() { } ); } + +#[test] +fn unused_record_fields_5() { + let source_code = r#" + pub type Foo { + b_is_before_a: Bool, + a_is_after_b: Bool, + } + + test confusing() { + let Foo(a, b) = Foo(True, True) + a || b + } + "#; + + let result = check_validator(parse(source_code)); + assert!(result.is_ok()); + + let (warnings, _) = result.unwrap(); + assert_eq!( + warnings[0], + Warning::UnusedRecordFields { + location: Span::create(137, 9), + suggestion: UntypedPattern::Constructor { + is_record: true, + location: Span::create(137, 9), + name: "Foo".to_string(), + arguments: vec![ + CallArg { + label: Some("b_is_before_a".to_string()), + location: Span::create(141, 13), + value: Pattern::Var { + location: Span::create(141, 1), + name: "a".to_string() + } + }, + CallArg { + label: Some("a_is_after_b".to_string()), + location: Span::create(144, 12), + value: Pattern::Var { + location: Span::create(144, 1), + name: "b".to_string() + } + } + ], + module: None, + constructor: (), + spread_location: None, + tipo: () + } + } + ); +} diff --git a/crates/aiken-lang/src/tipo/pattern.rs b/crates/aiken-lang/src/tipo/pattern.rs index 99ad74ef..a223a58e 100644 --- a/crates/aiken-lang/src/tipo/pattern.rs +++ b/crates/aiken-lang/src/tipo/pattern.rs @@ -467,7 +467,7 @@ impl<'a, 'b> PatternTyper<'a, 'b> { let arguments = field_map .fields .iter() - .sorted_by(|(a, _), (b, _)| a.cmp(b)) + .sorted_by(|(_, (a, _)), (_, (b, _))| a.cmp(b)) .zip(pattern_args.iter()) .filter_map(|((field, (_, _)), arg)| { if arg.value.is_discard() {