From 5a3c5e6666f53ac222e297e69971e3d84499759f Mon Sep 17 00:00:00 2001 From: InversionSpaces Date: Wed, 1 Nov 2023 12:55:59 +0100 Subject: [PATCH] fix(compiler): Unknown service method call is ignored [LNG-273] (#957) * Remove property token adjustment * Revert "Remove property token adjustment" This reverts commit 27d72f2bff4993fb58fd5125e0fe4ffae5c2aa77. * Remove scope word * Refactor * Refactor, add error * Add unit tests * Fix specifier for struct --- .../aqua/semantics/rules/ValuesAlgebra.scala | 173 ++++++++++-------- .../rules/types/TypesInterpreter.scala | 2 +- .../scala/aqua/semantics/SemanticsSpec.scala | 43 +++++ types/src/main/scala/aqua/types/Type.scala | 17 +- .../src/main/scala/aqua/syntax/optiont.scala | 31 ++++ 5 files changed, 187 insertions(+), 79 deletions(-) create mode 100644 utils/helpers/src/main/scala/aqua/syntax/optiont.scala diff --git a/semantics/src/main/scala/aqua/semantics/rules/ValuesAlgebra.scala b/semantics/src/main/scala/aqua/semantics/rules/ValuesAlgebra.scala index f5b2d7b7..2b7e643b 100644 --- a/semantics/src/main/scala/aqua/semantics/rules/ValuesAlgebra.scala +++ b/semantics/src/main/scala/aqua/semantics/rules/ValuesAlgebra.scala @@ -9,6 +9,8 @@ import aqua.semantics.rules.names.NamesAlgebra import aqua.semantics.rules.report.ReportAlgebra import aqua.semantics.rules.types.TypesAlgebra import aqua.types.* +import aqua.helpers.syntax.optiont.* + import cats.Monad import cats.data.{NonEmptyList, OptionT} import cats.instances.list.* @@ -336,92 +338,113 @@ class ValuesAlgebra[S[_], Alg[_]: Monad](using def ensureIsString(v: ValueToken[S]): Alg[Boolean] = valueToStringRaw(v).map(_.isDefined) - private def callArrowFromAbility( + private def abilityArrow( ab: Name[S], at: NamedType, funcName: Name[S] - ): Option[CallArrowRaw] = at.arrows - .get(funcName.value) - .map(arrowType => - CallArrowRaw.ability( - ab.value, - funcName.value, - arrowType + ): OptionT[Alg, CallArrowRaw] = + OptionT + .fromOption( + at.arrows.get(funcName.value) + ) + .map(arrowType => + CallArrowRaw.ability( + ab.value, + funcName.value, + arrowType + ) + ) + .flatTapNone( + report.error( + funcName, + s"Function `${funcName.value}` is not defined " + + s"in `${ab.value}` of type `${at.fullName}`, " + + s"available functions: ${at.arrows.keys.mkString(", ")}" + ) + ) + + private def callArrowFromFunc( + funcName: Name[S] + ): OptionT[Alg, CallArrowRaw] = + OptionT( + N.readArrow(funcName) + ).map(arrowType => + CallArrowRaw.func( + funcName = funcName.value, + baseType = arrowType ) ) + private def callArrowFromAbility( + ab: NamedTypeToken[S], + funcName: Name[S] + ): OptionT[Alg, CallArrowRaw] = { + lazy val nameTypeFromAbility = OptionT( + N.read(ab.asName, mustBeDefined = false) + ).collect { case nt: (AbilityType | ServiceType) => ab.asName -> nt } + + lazy val nameTypeFromService = for { + st <- OptionT( + T.getType(ab.value) + ).collect { case st: ServiceType => st } + rename <- OptionT( + A.getServiceRename(ab) + ) + renamed = ab.asName.rename(rename) + } yield renamed -> st + + lazy val nameType = nameTypeFromAbility orElse nameTypeFromService.widen + + lazy val fromArrow = OptionT( + A.getArrow(ab, funcName) + ).map(at => + CallArrowRaw + .ability( + abilityName = ab.value, + funcName = funcName.value, + baseType = at + ) + ) + + /** + * If we have a name and a type, get function from ability. + * Otherwise, get function from arrow. + * + * It is done like so to not report irrelevant errors. + */ + nameType.flatTransformT { + case Some((name, nt)) => abilityArrow(name, nt, funcName) + case _ => fromArrow + } + } + private def callArrowToRaw( callArrow: CallArrowToken[S] ): Alg[Option[CallArrowRaw]] = - for { - raw <- callArrow.ability.fold( - for { - myabeArrowType <- N.readArrow(callArrow.funcName) - } yield myabeArrowType - .map(arrowType => - CallArrowRaw.func( - funcName = callArrow.funcName.value, - baseType = arrowType + (for { + raw <- callArrow.ability + .fold(callArrowFromFunc(callArrow.funcName))(ab => + callArrowFromAbility(ab, callArrow.funcName) + ) + domain = raw.baseType.domain + _ <- OptionT.withFilterF( + T.checkArgumentsNumber( + callArrow.funcName, + domain.length, + callArrow.args.length + ) + ) + args <- callArrow.args + .zip(domain.toList) + .traverse { case (tkn, tp) => + for { + valueRaw <- OptionT(valueToRaw(tkn)) + _ <- OptionT.withFilterF( + T.ensureTypeMatches(tkn, tp, valueRaw.`type`) ) - ) - )(ab => - N.read(ab.asName, mustBeDefined = false).flatMap { - case Some(nt: (AbilityType | ServiceType)) => - callArrowFromAbility(ab.asName, nt, callArrow.funcName).pure - case _ => - T.getType(ab.value).flatMap { - case Some(st: ServiceType) => - OptionT(A.getServiceRename(ab)) - .subflatMap(rename => - callArrowFromAbility( - ab.asName.rename(rename), - st, - callArrow.funcName - ) - ) - .value - case _ => - A.getArrow(ab, callArrow.funcName).map { - case Some(at) => - CallArrowRaw - .ability( - abilityName = ab.value, - funcName = callArrow.funcName.value, - baseType = at - ) - .some - case _ => none - } - } + } yield valueRaw } - ) - result <- raw.flatTraverse(r => - val arr = r.baseType - for { - argsCheck <- T.checkArgumentsNumber( - callArrow.funcName, - arr.domain.length, - callArrow.args.length - ) - args <- Option - .when(argsCheck)(callArrow.args zip arr.domain.toList) - .traverse( - _.flatTraverse { case (tkn, tp) => - for { - maybeValueRaw <- valueToRaw(tkn) - checked <- maybeValueRaw.flatTraverse(v => - T.ensureTypeMatches(tkn, tp, v.`type`) - .map(Option.when(_)(v)) - ) - } yield checked.toList - } - ) - result = args - .filter(_.length == arr.domain.length) - .map(args => r.copy(arguments = args)) - } yield result - ) - } yield result + } yield raw.copy(arguments = args)).value } 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 39d1182a..add0cc17 100644 --- a/semantics/src/main/scala/aqua/semantics/rules/types/TypesInterpreter.scala +++ b/semantics/src/main/scala/aqua/semantics/rules/types/TypesInterpreter.scala @@ -241,7 +241,7 @@ class TypesInterpreter[S[_], X](using report .error( op, - s"Expected scope type to resolve an arrow '${op.name.value}' or a type with this property. Got: $rootT" + s"Expected type to resolve an arrow '${op.name.value}' or a type with this property. Got: $rootT" ) .as(None) )(t => State.pure(Some(FunctorRaw(op.name.value, t)))) diff --git a/semantics/src/test/scala/aqua/semantics/SemanticsSpec.scala b/semantics/src/test/scala/aqua/semantics/SemanticsSpec.scala index 915c9b42..d2315075 100644 --- a/semantics/src/test/scala/aqua/semantics/SemanticsSpec.scala +++ b/semantics/src/test/scala/aqua/semantics/SemanticsSpec.scala @@ -796,4 +796,47 @@ class SemanticsSpec extends AnyFlatSpec with Matchers with Inside { } } + + it should "report an error on unknown service methods" in { + val script = """ + |service Test("test"): + | call(i: i32) -> i32 + | + |func test(): + | Test.unknown("test") + |""".stripMargin + + insideSemErrors(script) { errors => + errors.toChain.toList.exists { + case RulesViolated(_, messages) => + messages.exists(_.contains("not defined")) && + messages.exists(_.contains("unknown")) + case _ => false + } + } + } + + it should "report an error on unknown ability arrows" in { + val script = """ + |ability Test: + | call(i: i32) -> i32 + | + |func test(): + | call = (i: i32) -> i32: + | <- i + | + | t = Test(call) + | + | t.unknown("test") + |""".stripMargin + + insideSemErrors(script) { errors => + errors.toChain.toList.exists { + case RulesViolated(_, messages) => + messages.exists(_.contains("not defined")) && + messages.exists(_.contains("unknown")) + case _ => false + } + } + } } diff --git a/types/src/main/scala/aqua/types/Type.scala b/types/src/main/scala/aqua/types/Type.scala index c0492e63..7a7a1fe4 100644 --- a/types/src/main/scala/aqua/types/Type.scala +++ b/types/src/main/scala/aqua/types/Type.scala @@ -286,7 +286,12 @@ case class OptionType(element: Type) extends BoxType { } sealed trait NamedType extends Type { + + def specifier: String def name: String + + final def fullName: String = s"$specifier $name" + def fields: NonEmptyMap[String, Type] /** @@ -363,8 +368,10 @@ sealed trait NamedType extends Type { case class StructType(name: String, fields: NonEmptyMap[String, Type]) extends DataType with NamedType { + override val specifier: String = "data" + override def toString: String = - s"$name{${fields.map(_.toString).toNel.toList.map(kv => kv._1 + ": " + kv._2).mkString(", ")}}" + s"$fullName{${fields.map(_.toString).toNel.toList.map(kv => kv._1 + ": " + kv._2).mkString(", ")}}" } case class StreamMapType(element: Type) extends DataType { @@ -378,15 +385,19 @@ object StreamMapType { case class ServiceType(name: String, fields: NonEmptyMap[String, ArrowType]) extends NamedType { + override val specifier: String = "service" + override def toString: String = - s"service $name{${fields.map(_.toString).toNel.toList.map(kv => kv._1 + ": " + kv._2).mkString(", ")}}" + s"$fullName{${fields.map(_.toString).toNel.toList.map(kv => kv._1 + ": " + kv._2).mkString(", ")}}" } // Ability is an unordered collection of labelled types and arrows case class AbilityType(name: String, fields: NonEmptyMap[String, Type]) extends NamedType { + override val specifier: String = "ability" + override def toString: String = - s"ability $name{${fields.map(_.toString).toNel.toList.map(kv => kv._1 + ": " + kv._2).mkString(", ")}}" + s"$fullName{${fields.map(_.toString).toNel.toList.map(kv => kv._1 + ": " + kv._2).mkString(", ")}}" } object AbilityType { diff --git a/utils/helpers/src/main/scala/aqua/syntax/optiont.scala b/utils/helpers/src/main/scala/aqua/syntax/optiont.scala new file mode 100644 index 00000000..2b499f51 --- /dev/null +++ b/utils/helpers/src/main/scala/aqua/syntax/optiont.scala @@ -0,0 +1,31 @@ +package aqua.helpers.syntax + +import cats.{Functor, Monad} +import cats.data.OptionT +import cats.syntax.functor.* + +object optiont { + + extension (o: OptionT.type) { + + /** + * Lifts a `F[Boolean]` into a `OptionT[F, Unit]` that is `None` if the + * condition is `false` and `Some(())` otherwise. + * + * This is useful for filtering a `OptionT[F, A]` inside a for-comprehension. + */ + def withFilterF[F[_]: Functor](fb: F[Boolean]): OptionT[F, Unit] = + OptionT.liftF(fb).filter(identity).void + } + + extension [F[_], A](o: OptionT[F, A]) { + + /** + * Like `flatTransform` but the transformation function returns a `OptionT[F, B]`. + */ + def flatTransformT[B]( + f: Option[A] => OptionT[F, B] + )(using F: Monad[F]): OptionT[F, B] = + o.flatTransform(f.andThen(_.value)) + } +}