From d5cd77bb865433fdff46fefb48875bf8f5e585dc Mon Sep 17 00:00:00 2001 From: InversionSpaces Date: Tue, 9 Jan 2024 13:15:27 +0100 Subject: [PATCH] feat(compiler): Enhance message of type error [LNG-313] (#1033) --- .../aqua/semantics/rules/ValuesAlgebra.scala | 35 +++++++--------- .../semantics/rules/types/TypesAlgebra.scala | 16 +++++++ .../rules/types/TypesInterpreter.scala | 42 +++++++++++++++++++ 3 files changed, 74 insertions(+), 19 deletions(-) diff --git a/semantics/src/main/scala/aqua/semantics/rules/ValuesAlgebra.scala b/semantics/src/main/scala/aqua/semantics/rules/ValuesAlgebra.scala index 4ddded3d..12cbf8d4 100644 --- a/semantics/src/main/scala/aqua/semantics/rules/ValuesAlgebra.scala +++ b/semantics/src/main/scala/aqua/semantics/rules/ValuesAlgebra.scala @@ -17,6 +17,7 @@ import cats.data.{NonEmptyList, OptionT} import cats.instances.list.* import cats.syntax.applicative.* import cats.syntax.apply.* +import cats.syntax.bifunctor.* import cats.syntax.flatMap.* import cats.syntax.foldable.* import cats.syntax.functor.* @@ -136,27 +137,23 @@ class ValuesAlgebra[S[_], Alg[_]: Monad](using reportNamedArgsDuplicates(fields) ) fieldsGiven <- fields - .traverse(arg => OptionT(valueToRaw(arg.argValue)).map(arg.argName.value -> _)) + .traverse(arg => + OptionT(valueToRaw(arg.argValue)).map(valueRaw => + arg.argName.value -> (arg, valueRaw) + ) + ) .map(_.toNem) // Take only last value for a field - fieldsGivenTypes = fieldsGiven.map(_.`type`) - generated <- OptionT.fromOption( - resolvedType match { - case struct: StructType => - ( - struct.copy(fields = fieldsGivenTypes), - MakeStructRaw(fieldsGiven, struct) - ).some - case ability: AbilityType => - ( - ability.copy(fields = fieldsGivenTypes), - AbilityRaw(fieldsGiven, ability) - ).some - } - ) - (genType, genData) = generated + fieldsGivenRaws = fieldsGiven.map { case (_, raw) => raw } + fieldsGivenTypes = fieldsGiven.map(_.map(_.`type`)) + generated = resolvedType match { + case struct: StructType => + MakeStructRaw(fieldsGivenRaws, struct) + case ability: AbilityType => + AbilityRaw(fieldsGivenRaws, ability) + } data <- OptionT.whenM( - T.ensureTypeMatches(dvt, resolvedType, genType) - )(genData.pure) + T.ensureTypeConstructibleFrom(dvt, resolvedType, fieldsGivenTypes) + )(generated.pure) } yield data).value case ct @ CollectionToken(_, values) => diff --git a/semantics/src/main/scala/aqua/semantics/rules/types/TypesAlgebra.scala b/semantics/src/main/scala/aqua/semantics/rules/types/TypesAlgebra.scala index 742ae69d..b4eb4dbd 100644 --- a/semantics/src/main/scala/aqua/semantics/rules/types/TypesAlgebra.scala +++ b/semantics/src/main/scala/aqua/semantics/rules/types/TypesAlgebra.scala @@ -60,6 +60,22 @@ trait TypesAlgebra[S[_], Alg[_]] { def ensureTypeMatches(token: Token[S], expected: Type, givenType: Type): Alg[Boolean] + /** + * Check if given type (ability or struct) + * can be constructed from given arguments + * + * @param token token of construction expression (for error reporting) + * @param expected type to construct + * @param arguments arguments to construct with (name -> (named arg, type)) + * @return true if type can be constructed from given arguments + * reports error and warnings if necessary + */ + def ensureTypeConstructibleFrom( + token: Token[S], + expected: AbilityType | StructType, + arguments: NonEmptyMap[String, (NamedArg[S], Type)] + ): Alg[Boolean] + def typeToCollectible(token: Token[S], givenType: Type): OptionT[Alg, CollectibleType] def typeToStream(token: Token[S], givenType: Type): OptionT[Alg, StreamType] diff --git a/semantics/src/main/scala/aqua/semantics/rules/types/TypesInterpreter.scala b/semantics/src/main/scala/aqua/semantics/rules/types/TypesInterpreter.scala index 4f76de4d..24e41526 100644 --- a/semantics/src/main/scala/aqua/semantics/rules/types/TypesInterpreter.scala +++ b/semantics/src/main/scala/aqua/semantics/rules/types/TypesInterpreter.scala @@ -2,6 +2,7 @@ package aqua.semantics.rules.types import aqua.parser.lexer.* import aqua.raw.value.* +import aqua.semantics.Levenshtein import aqua.semantics.rules.StackInterpreter import aqua.semantics.rules.locations.{DefinitionInfo, LocationsAlgebra} import aqua.semantics.rules.report.ReportAlgebra @@ -415,6 +416,47 @@ class TypesInterpreter[S[_], X](using } } + override def ensureTypeConstructibleFrom( + token: Token[S], + expected: AbilityType | StructType, + arguments: NonEmptyMap[String, (NamedArg[S], Type)] + ): State[X, Boolean] = for { + /* Check that required fields are present + among arguments and have correct types */ + enough <- expected.fields.toNel.traverse { case (name, typ) => + arguments.lookup(name) match { + case Some(arg -> givenType) => + ensureTypeMatches(arg.argValue, typ, givenType) + case None => + report + .error( + token, + s"Missing argument '$name' of type '$typ'" + ) + .as(false) + } + }.map(_.forall(identity)) + expectedKeys = expected.fields.keys.toNonEmptyList + /* Report unexpected arguments */ + _ <- arguments.toNel.traverse_ { case (name, arg -> typ) => + expected.fields.lookup(name) match { + case Some(_) => State.pure(()) + case None => + lazy val similar = Levenshtein + .mostSimilar(name, expectedKeys, 3) + .map(s => s"'$s'") + .mkString(", ") + val message = + if (enough) + s"Unexpected argument '$name'" + else + s"Unexpected argument '$name', did you mean $similar?" + + report.warning(arg.argName, message) + } + } + } yield enough + private def typeTo[T <: Type]( token: Token[S], givenType: Type,