fix(compiler): Unknown service method call is ignored [LNG-273] (#957)

* Remove property token adjustment

* Revert "Remove property token adjustment"

This reverts commit 27d72f2bff.

* Remove scope word

* Refactor

* Refactor, add error

* Add unit tests

* Fix specifier for struct
This commit is contained in:
InversionSpaces 2023-11-01 12:55:59 +01:00 committed by GitHub
parent 077dc8ff13
commit 5a3c5e6666
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 187 additions and 79 deletions

View File

@ -9,6 +9,8 @@ import aqua.semantics.rules.names.NamesAlgebra
import aqua.semantics.rules.report.ReportAlgebra import aqua.semantics.rules.report.ReportAlgebra
import aqua.semantics.rules.types.TypesAlgebra import aqua.semantics.rules.types.TypesAlgebra
import aqua.types.* import aqua.types.*
import aqua.helpers.syntax.optiont.*
import cats.Monad import cats.Monad
import cats.data.{NonEmptyList, OptionT} import cats.data.{NonEmptyList, OptionT}
import cats.instances.list.* import cats.instances.list.*
@ -336,12 +338,15 @@ class ValuesAlgebra[S[_], Alg[_]: Monad](using
def ensureIsString(v: ValueToken[S]): Alg[Boolean] = def ensureIsString(v: ValueToken[S]): Alg[Boolean] =
valueToStringRaw(v).map(_.isDefined) valueToStringRaw(v).map(_.isDefined)
private def callArrowFromAbility( private def abilityArrow(
ab: Name[S], ab: Name[S],
at: NamedType, at: NamedType,
funcName: Name[S] funcName: Name[S]
): Option[CallArrowRaw] = at.arrows ): OptionT[Alg, CallArrowRaw] =
.get(funcName.value) OptionT
.fromOption(
at.arrows.get(funcName.value)
)
.map(arrowType => .map(arrowType =>
CallArrowRaw.ability( CallArrowRaw.ability(
ab.value, ab.value,
@ -349,79 +354,97 @@ class ValuesAlgebra[S[_], Alg[_]: Monad](using
arrowType 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( private def callArrowToRaw(
callArrow: CallArrowToken[S] callArrow: CallArrowToken[S]
): Alg[Option[CallArrowRaw]] = ): Alg[Option[CallArrowRaw]] =
for { (for {
raw <- callArrow.ability.fold( raw <- callArrow.ability
for { .fold(callArrowFromFunc(callArrow.funcName))(ab =>
myabeArrowType <- N.readArrow(callArrow.funcName) callArrowFromAbility(ab, callArrow.funcName)
} yield myabeArrowType
.map(arrowType =>
CallArrowRaw.func(
funcName = callArrow.funcName.value,
baseType = arrowType
) )
) domain = raw.baseType.domain
)(ab => _ <- OptionT.withFilterF(
N.read(ab.asName, mustBeDefined = false).flatMap { T.checkArgumentsNumber(
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
}
}
}
)
result <- raw.flatTraverse(r =>
val arr = r.baseType
for {
argsCheck <- T.checkArgumentsNumber(
callArrow.funcName, callArrow.funcName,
arr.domain.length, domain.length,
callArrow.args.length callArrow.args.length
) )
args <- Option )
.when(argsCheck)(callArrow.args zip arr.domain.toList) args <- callArrow.args
.traverse( .zip(domain.toList)
_.flatTraverse { case (tkn, tp) => .traverse { case (tkn, tp) =>
for { for {
maybeValueRaw <- valueToRaw(tkn) valueRaw <- OptionT(valueToRaw(tkn))
checked <- maybeValueRaw.flatTraverse(v => _ <- OptionT.withFilterF(
T.ensureTypeMatches(tkn, tp, v.`type`) T.ensureTypeMatches(tkn, tp, valueRaw.`type`)
.map(Option.when(_)(v))
) )
} yield checked.toList } yield valueRaw
} }
) } yield raw.copy(arguments = args)).value
result = args
.filter(_.length == arr.domain.length)
.map(args => r.copy(arguments = args))
} yield result
)
} yield result
} }

View File

@ -241,7 +241,7 @@ class TypesInterpreter[S[_], X](using
report report
.error( .error(
op, 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) .as(None)
)(t => State.pure(Some(FunctorRaw(op.name.value, t)))) )(t => State.pure(Some(FunctorRaw(op.name.value, t))))

View File

@ -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
}
}
}
} }

View File

@ -286,7 +286,12 @@ case class OptionType(element: Type) extends BoxType {
} }
sealed trait NamedType extends Type { sealed trait NamedType extends Type {
def specifier: String
def name: String def name: String
final def fullName: String = s"$specifier $name"
def fields: NonEmptyMap[String, Type] def fields: NonEmptyMap[String, Type]
/** /**
@ -363,8 +368,10 @@ sealed trait NamedType extends Type {
case class StructType(name: String, fields: NonEmptyMap[String, Type]) case class StructType(name: String, fields: NonEmptyMap[String, Type])
extends DataType with NamedType { extends DataType with NamedType {
override val specifier: String = "data"
override def toString: String = 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 { case class StreamMapType(element: Type) extends DataType {
@ -378,15 +385,19 @@ object StreamMapType {
case class ServiceType(name: String, fields: NonEmptyMap[String, ArrowType]) extends NamedType { case class ServiceType(name: String, fields: NonEmptyMap[String, ArrowType]) extends NamedType {
override val specifier: String = "service"
override def toString: String = 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 // Ability is an unordered collection of labelled types and arrows
case class AbilityType(name: String, fields: NonEmptyMap[String, Type]) extends NamedType { case class AbilityType(name: String, fields: NonEmptyMap[String, Type]) extends NamedType {
override val specifier: String = "ability"
override def toString: String = 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 { object AbilityType {

View File

@ -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))
}
}