From 27d72f2bff4993fb58fd5125e0fe4ffae5c2aa77 Mon Sep 17 00:00:00 2001 From: InversionSpaces Date: Tue, 31 Oct 2023 11:44:32 +0000 Subject: [PATCH 1/7] Remove property token adjustment --- .../aqua/semantics/rules/ValuesAlgebra.scala | 43 +++++++++---------- 1 file changed, 20 insertions(+), 23 deletions(-) diff --git a/semantics/src/main/scala/aqua/semantics/rules/ValuesAlgebra.scala b/semantics/src/main/scala/aqua/semantics/rules/ValuesAlgebra.scala index f5b2d7b74..05f841faf 100644 --- a/semantics/src/main/scala/aqua/semantics/rules/ValuesAlgebra.scala +++ b/semantics/src/main/scala/aqua/semantics/rules/ValuesAlgebra.scala @@ -96,20 +96,18 @@ class ValuesAlgebra[S[_], Alg[_]: Monad](using } case prop @ PropertyToken(value, properties) => - prop.adjust.fold( - for { - valueRaw <- valueToRaw(value) - result <- valueRaw.flatTraverse(raw => - properties - .foldLeftM(raw) { case (prev, op) => - OptionT( - resolveSingleProperty(prev.`type`, op) - ).map(prop => ApplyPropertyRaw(prev, prop)) - } - .value - ) - } yield result - )(valueToRaw) + for { + valueRaw <- valueToRaw(value) + result <- valueRaw.flatTraverse(raw => + properties + .foldLeftM(raw) { case (prev, op) => + OptionT( + resolveSingleProperty(prev.`type`, op) + ).map(prop => ApplyPropertyRaw(prev, prop)) + } + .value + ) + } yield result case dvt @ NamedValueToken(typeName, fields) => (for { @@ -311,15 +309,14 @@ class ValuesAlgebra[S[_], Alg[_]: Monad](using } def valueToCall(v: ValueToken[S]): Alg[Option[(ValueRaw, ArrowType)]] = - valueToRaw(v).flatMap( - _.flatTraverse { - case ca: CallArrowRaw => (ca, ca.baseType).some.pure[Alg] - case apr @ ApplyPropertyRaw(_, IntoArrowRaw(_, arrowType, _)) => - (apr, arrowType).some.pure[Alg] - // TODO: better error message (`raw` formatting) - case raw => report.error(v, s"Expected arrow call, got $raw").as(none) - } - ) + valueToRaw(v).flatMap(_.flatTraverse { + case ca: CallArrowRaw => + (ca, ca.baseType).some.pure[Alg] + case apr @ ApplyPropertyRaw(_, IntoArrowRaw(_, arrowType, _)) => + (apr, arrowType).some.pure[Alg] + // TODO: better error message (`raw` formatting) + case raw => report.error(v, s"Expected arrow call, got $raw").as(none) + }) def valueToTypedRaw(v: ValueToken[S], expectedType: Type): Alg[Option[ValueRaw]] = OptionT(valueToRaw(v)) From 9e8242332d871b7a6be4045e919f51570852e7a2 Mon Sep 17 00:00:00 2001 From: InversionSpaces Date: Tue, 31 Oct 2023 15:50:27 +0000 Subject: [PATCH 2/7] Revert "Remove property token adjustment" This reverts commit 27d72f2bff4993fb58fd5125e0fe4ffae5c2aa77. --- .../aqua/semantics/rules/ValuesAlgebra.scala | 43 ++++++++++--------- 1 file changed, 23 insertions(+), 20 deletions(-) diff --git a/semantics/src/main/scala/aqua/semantics/rules/ValuesAlgebra.scala b/semantics/src/main/scala/aqua/semantics/rules/ValuesAlgebra.scala index 05f841faf..f5b2d7b74 100644 --- a/semantics/src/main/scala/aqua/semantics/rules/ValuesAlgebra.scala +++ b/semantics/src/main/scala/aqua/semantics/rules/ValuesAlgebra.scala @@ -96,18 +96,20 @@ class ValuesAlgebra[S[_], Alg[_]: Monad](using } case prop @ PropertyToken(value, properties) => - for { - valueRaw <- valueToRaw(value) - result <- valueRaw.flatTraverse(raw => - properties - .foldLeftM(raw) { case (prev, op) => - OptionT( - resolveSingleProperty(prev.`type`, op) - ).map(prop => ApplyPropertyRaw(prev, prop)) - } - .value - ) - } yield result + prop.adjust.fold( + for { + valueRaw <- valueToRaw(value) + result <- valueRaw.flatTraverse(raw => + properties + .foldLeftM(raw) { case (prev, op) => + OptionT( + resolveSingleProperty(prev.`type`, op) + ).map(prop => ApplyPropertyRaw(prev, prop)) + } + .value + ) + } yield result + )(valueToRaw) case dvt @ NamedValueToken(typeName, fields) => (for { @@ -309,14 +311,15 @@ class ValuesAlgebra[S[_], Alg[_]: Monad](using } def valueToCall(v: ValueToken[S]): Alg[Option[(ValueRaw, ArrowType)]] = - valueToRaw(v).flatMap(_.flatTraverse { - case ca: CallArrowRaw => - (ca, ca.baseType).some.pure[Alg] - case apr @ ApplyPropertyRaw(_, IntoArrowRaw(_, arrowType, _)) => - (apr, arrowType).some.pure[Alg] - // TODO: better error message (`raw` formatting) - case raw => report.error(v, s"Expected arrow call, got $raw").as(none) - }) + valueToRaw(v).flatMap( + _.flatTraverse { + case ca: CallArrowRaw => (ca, ca.baseType).some.pure[Alg] + case apr @ ApplyPropertyRaw(_, IntoArrowRaw(_, arrowType, _)) => + (apr, arrowType).some.pure[Alg] + // TODO: better error message (`raw` formatting) + case raw => report.error(v, s"Expected arrow call, got $raw").as(none) + } + ) def valueToTypedRaw(v: ValueToken[S], expectedType: Type): Alg[Option[ValueRaw]] = OptionT(valueToRaw(v)) From a4c3452fdb4191e0ce6cb23c152d043af5d0d1d4 Mon Sep 17 00:00:00 2001 From: InversionSpaces Date: Tue, 31 Oct 2023 15:51:01 +0000 Subject: [PATCH 3/7] Remove scope word --- .../scala/aqua/semantics/rules/types/TypesInterpreter.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 39d1182a5..add0cc17d 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)))) From 391b6c80688b3ec5e5723ab0d4f469f6571ff2b2 Mon Sep 17 00:00:00 2001 From: InversionSpaces Date: Wed, 1 Nov 2023 10:18:41 +0000 Subject: [PATCH 4/7] Refactor --- .../aqua/semantics/rules/ValuesAlgebra.scala | 148 ++++++++++-------- .../src/main/scala/aqua/syntax/optiont.scala | 20 +++ 2 files changed, 100 insertions(+), 68 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 f5b2d7b74..cffd6f6d4 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,7 +338,7 @@ 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] @@ -350,78 +352,88 @@ class ValuesAlgebra[S[_], Alg[_]: Monad](using ) ) + 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 fromAbility = for { + nt <- OptionT( + N.read(ab.asName, mustBeDefined = false) + ).collect { case nt: (AbilityType | ServiceType) => nt } + ca <- OptionT.fromOption( + abilityArrow(ab.asName, nt, funcName) + ) + } yield ca + + lazy val fromService = for { + st <- OptionT( + T.getType(ab.value) + ).collect { case st: ServiceType => st } + rename <- OptionT( + A.getServiceRename(ab) + ) + ca <- OptionT.fromOption( + abilityArrow( + ab.asName.rename(rename), + st, + funcName + ) + ) + } yield ca + + lazy val fromArrow = OptionT( + A.getArrow(ab, funcName) + ).map(at => + CallArrowRaw + .ability( + abilityName = ab.value, + funcName = funcName.value, + baseType = at + ) + ) + + fromAbility.orElse(fromService).orElse(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 - ) - ) - )(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 - } - } - } + (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 + ) ) - 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 - } + args <- callArrow.args + .zip(domain.toList) + .traverse { case (tkn, tp) => + for { + valueRaw <- OptionT(valueToRaw(tkn)) + _ <- OptionT.withFilterF( + T.ensureTypeMatches(tkn, tp, valueRaw.`type`) ) - result = args - .filter(_.length == arr.domain.length) - .map(args => r.copy(arguments = args)) - } yield result - ) - } yield result + } yield valueRaw + } + } yield raw.copy(arguments = args)).value } 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 000000000..827e794bd --- /dev/null +++ b/utils/helpers/src/main/scala/aqua/syntax/optiont.scala @@ -0,0 +1,20 @@ +package aqua.helpers.syntax + +import cats.Functor +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 + } +} From 0716af278cdb68bf7eefcab74239b74a55c2e84b Mon Sep 17 00:00:00 2001 From: InversionSpaces Date: Wed, 1 Nov 2023 11:02:34 +0000 Subject: [PATCH 5/7] Refactor, add error --- .../aqua/semantics/rules/ValuesAlgebra.scala | 63 +++++++++++-------- types/src/main/scala/aqua/types/Type.scala | 17 ++++- .../src/main/scala/aqua/syntax/optiont.scala | 13 +++- 3 files changed, 63 insertions(+), 30 deletions(-) diff --git a/semantics/src/main/scala/aqua/semantics/rules/ValuesAlgebra.scala b/semantics/src/main/scala/aqua/semantics/rules/ValuesAlgebra.scala index cffd6f6d4..2b7e643bc 100644 --- a/semantics/src/main/scala/aqua/semantics/rules/ValuesAlgebra.scala +++ b/semantics/src/main/scala/aqua/semantics/rules/ValuesAlgebra.scala @@ -342,15 +342,26 @@ class ValuesAlgebra[S[_], Alg[_]: Monad](using 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] @@ -368,30 +379,21 @@ class ValuesAlgebra[S[_], Alg[_]: Monad](using ab: NamedTypeToken[S], funcName: Name[S] ): OptionT[Alg, CallArrowRaw] = { - lazy val fromAbility = for { - nt <- OptionT( - N.read(ab.asName, mustBeDefined = false) - ).collect { case nt: (AbilityType | ServiceType) => nt } - ca <- OptionT.fromOption( - abilityArrow(ab.asName, nt, funcName) - ) - } yield ca + lazy val nameTypeFromAbility = OptionT( + N.read(ab.asName, mustBeDefined = false) + ).collect { case nt: (AbilityType | ServiceType) => ab.asName -> nt } - lazy val fromService = for { + lazy val nameTypeFromService = for { st <- OptionT( T.getType(ab.value) ).collect { case st: ServiceType => st } rename <- OptionT( A.getServiceRename(ab) ) - ca <- OptionT.fromOption( - abilityArrow( - ab.asName.rename(rename), - st, - funcName - ) - ) - } yield ca + renamed = ab.asName.rename(rename) + } yield renamed -> st + + lazy val nameType = nameTypeFromAbility orElse nameTypeFromService.widen lazy val fromArrow = OptionT( A.getArrow(ab, funcName) @@ -404,7 +406,16 @@ class ValuesAlgebra[S[_], Alg[_]: Monad](using ) ) - fromAbility.orElse(fromService).orElse(fromArrow) + /** + * 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( diff --git a/types/src/main/scala/aqua/types/Type.scala b/types/src/main/scala/aqua/types/Type.scala index c0492e637..166873136 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 = "struct" + 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 index 827e794bd..2b499f514 100644 --- a/utils/helpers/src/main/scala/aqua/syntax/optiont.scala +++ b/utils/helpers/src/main/scala/aqua/syntax/optiont.scala @@ -1,6 +1,6 @@ package aqua.helpers.syntax -import cats.Functor +import cats.{Functor, Monad} import cats.data.OptionT import cats.syntax.functor.* @@ -17,4 +17,15 @@ object optiont { 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)) + } } From 10a685e0a727a02294a204c0e9247011e7add9c6 Mon Sep 17 00:00:00 2001 From: InversionSpaces Date: Wed, 1 Nov 2023 11:31:14 +0000 Subject: [PATCH 6/7] Add unit tests --- .../scala/aqua/semantics/SemanticsSpec.scala | 43 +++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/semantics/src/test/scala/aqua/semantics/SemanticsSpec.scala b/semantics/src/test/scala/aqua/semantics/SemanticsSpec.scala index 915c9b420..d2315075c 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 + } + } + } } From bda2f3c1a4d02f49872dcd6239e570674c076e0b Mon Sep 17 00:00:00 2001 From: InversionSpaces Date: Wed, 1 Nov 2023 11:35:32 +0000 Subject: [PATCH 7/7] Fix specifier for struct --- types/src/main/scala/aqua/types/Type.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/types/src/main/scala/aqua/types/Type.scala b/types/src/main/scala/aqua/types/Type.scala index 166873136..7a7a1fe40 100644 --- a/types/src/main/scala/aqua/types/Type.scala +++ b/types/src/main/scala/aqua/types/Type.scala @@ -368,7 +368,7 @@ sealed trait NamedType extends Type { case class StructType(name: String, fields: NonEmptyMap[String, Type]) extends DataType with NamedType { - override val specifier: String = "struct" + override val specifier: String = "data" override def toString: String = s"$fullName{${fields.map(_.toString).toNel.toList.map(kv => kv._1 + ": " + kv._2).mkString(", ")}}"