diff --git a/CHANGELOG.md b/CHANGELOG.md index c0352036..4fcba714 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 @@ -17,6 +18,7 @@ - **aiken-lang**: Formatter was removing comments from function type annotation args @rvcas - **aiken-lang**: Parser wrongly merged two adjacent sequences together, effectively fusioning scopes. @KtorZ +- **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/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/ast.rs b/crates/aiken-lang/src/ast.rs index 7d45ab8c..d3882d51 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(Option, 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/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/expr.rs b/crates/aiken-lang/src/expr.rs index 26a62236..788fd2bb 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..09b184a8 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,15 @@ impl<'comments> Formatter<'comments> { } let name = match module { - Some(m) => m.to_doc().append(".").append(name), + 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/chain/field_access.rs b/crates/aiken-lang/src/parser/chain/field_access.rs index 86eab783..05c0708a 100644 --- a/crates/aiken-lang/src/parser/chain/field_access.rs +++ b/crates/aiken-lang/src/parser/chain/field_access.rs @@ -1,6 +1,6 @@ use super::Chain; use crate::{ - expr::UntypedExpr, + ast::well_known, parser::{token::Token, ParseError}, }; use chumsky::prelude::*; @@ -8,27 +8,13 @@ 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, }, + 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/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/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/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/parser/pattern/constructor.rs b/crates/aiken-lang/src/parser/pattern/constructor.rs index a0336fd2..d3ddfe9e 100644 --- a/crates/aiken-lang/src/parser/pattern/constructor.rs +++ b/crates/aiken-lang/src/parser/pattern/constructor.rs @@ -1,29 +1,72 @@ 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::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( + 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(None, 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 +145,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/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_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/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..8eb4d3cb --- /dev/null +++ b/crates/aiken-lang/src/parser/pattern/snapshots/constructor_type_select.snap @@ -0,0 +1,19 @@ +--- +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( + None, + "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..07d39e94 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()) } @@ -2254,6 +2267,524 @@ 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( + 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#" + pub type Foo { + A { a: Int } + B + } + "#; + + let source_code = r#" + use thing.{Foo} + + fn bar(foo: Foo) { + when foo is { + Foo.A { a } -> a > 10 + Foo.B -> False + } + } + "#; + + let result = check_with_deps(parse(source_code), vec![(parse_as(dependency, "thing"))]); + + assert!(matches!(result, Ok(..)), "{result:#?}"); +} + +#[test] +fn use_nested_type_as_namespace_for_patterns() { + let dependency = r#" + pub type Foo { + A { a: Int } + B + } + "#; + + let source_code = r#" + use foo.{Foo} + + fn bar(x: Foo) { + when x is { + foo.Foo.A { a } -> a > 10 + 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#" + pub opaque type Foo { + A { a: Int } + B + } + "#; + + let source_code = r#" + use thing.{Foo} + + fn bar(foo: Foo) { + when foo is { + Foo.A { a } -> a > 10 + 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 { a: Int } + 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 use_type_as_namespace_for_constructors() { + let dependency = r#" + pub type Foo { + I { i: Int } + B(Bool) + } + "#; + + let source_code = r#" + use foo.{Foo} + + test my_test() { + and { + Foo.I { 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 { i: Int } + B(Bool) + } + "#; + + let source_code = r#" + use foo + + test my_test() { + trace foo.Foo.I { i: 42 } + trace foo.Foo.B(False) + Void + } + "#; + + 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_from_multi_level_module() { + let dependency = r#" + pub type Foo { + I { i: Int } + B(Bool) + } + "#; + + let source_code = r#" + use foo/bar + + test my_test() { + trace bar.Foo.I { i: 42 } + trace bar.Foo.B(True) + 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 { i: Int } + B(Bool) + } + "#; + + let source_code = r#" + use foo as bar + + test my_test() { + trace bar.Foo.I { i: 42 } + trace bar.Foo.B(True) + 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 { i: Int } + B(Bool) + } + "#; + + let source_code = r#" + use foo.{Foo} + + test my_test() { + Foo.I(42) == Foo.A + } + "#; + + 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 { i: Int } + B(Bool) + } + + pub type Bar { + S { 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 { i: Int } + B(Bool) + } + + pub type Bar { + S { 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 { i: Int } + B(Bool) + } + "#; + + let source_code = r#" + use foo + + test my_test() { + trace foo.Foo.I { 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 { 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 { 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 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 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#" @@ -2264,7 +2795,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 @@ -2273,15 +2804,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 @@ -2290,10 +2818,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 { .. })), )); } @@ -3701,3 +4226,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/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")), + } +} diff --git a/crates/aiken-lang/src/tipo/environment.rs b/crates/aiken-lang/src/tipo/environment.rs index 8abec2f6..224bb5e1 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, @@ -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) } }) } @@ -372,13 +381,69 @@ 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(|| self.err_unknown_module(name.to_string(), location)) + } + + #[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 + .importable_modules + .get(module_name) + .ok_or_else(|| self.err_unknown_module(module_name.to_string(), module_location))?; + + 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, 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,22 +472,14 @@ 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) => { - 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); @@ -443,7 +500,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,19 +514,54 @@ impl<'a> Environment<'a> { constructors: self.local_constructor_names(), }), - Some(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, - })?; + 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(|| { + self.err_unknown_module(module_name.to_string(), module_location) + })?; + + 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 { + 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(), type_location), + (t, type_location), + (name, location.map(|start, end| (start + t.len() + 1, end))), + ) + } + + Some(Namespace::Module(m)) => { + 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); @@ -728,10 +820,28 @@ 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 in error hints. + if t.starts_with("__") { + None + } else { + Some(t.to_string()) + } + }) + .collect() + } + pub fn local_value_names(&self) -> Vec { 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() } @@ -739,7 +849,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() } @@ -1816,7 +1928,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() @@ -1840,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/error.rs b/crates/aiken-lang/src/tipo/error.rs index 21c22220..2a438f4a 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, }, @@ -805,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()) )] @@ -819,7 +822,7 @@ Perhaps, try the following: ) ))] UnknownModuleType { - #[label("unknown import")] + #[label("not exported?")] location: Span, name: String, module_name: String, @@ -1076,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{}{}", @@ -1092,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." @@ -1101,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 { @@ -1163,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, .. } @@ -1274,7 +1288,7 @@ fn suggest_pattern( expected: usize, name: &str, given: &[CallArg], - module: &Option, + module: &Option, is_record: bool, ) -> Option { if expected > given.len() { @@ -1309,7 +1323,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 @@ -1526,14 +1540,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()) @@ -1550,6 +1565,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 4c9d959e..43fe0dd6 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), @@ -1068,6 +1068,69 @@ 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.map(|start, end| (start + type_name.len() + 1, end)), + ), + ); + } + + 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() + { + 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 + .environment + .imported_modules + .get(module_name) + .ok_or_else(|| { + self.environment + .err_unknown_module(module_name.to_string(), *module_location) + })?; + + return self.infer_inner_type_constructor_access( + (module.name.as_str(), *module_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) + }), + ), + ); + } + } + + _ => (), + }; + // 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 +1138,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); @@ -1106,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 = @@ -1162,6 +1219,58 @@ 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 { + self.environment.increment_usage(type_name); + + 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.environment.unused_modules.remove(module_name); + + self.infer_module_access( + &self + .environment + .local_module_name(module_name, module_location)?, + label.to_string(), + &type_location, + label_location, + ) + } + #[allow(clippy::result_large_err)] fn infer_record_access( &mut self, @@ -1169,9 +1278,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) } @@ -2563,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 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); diff --git a/crates/aiken-lang/src/tipo/pattern.rs b/crates/aiken-lang/src/tipo/pattern.rs index 99ad74ef..6678d4a7 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}, @@ -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() { @@ -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, 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(),