mirror of
https://github.com/fluencelabs/aqua.git
synced 2024-12-04 14:40:17 +00:00
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:
parent
077dc8ff13
commit
5a3c5e6666
@ -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,92 +338,113 @@ 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
|
||||||
.map(arrowType =>
|
.fromOption(
|
||||||
CallArrowRaw.ability(
|
at.arrows.get(funcName.value)
|
||||||
ab.value,
|
)
|
||||||
funcName.value,
|
.map(arrowType =>
|
||||||
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(
|
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 =>
|
domain = raw.baseType.domain
|
||||||
CallArrowRaw.func(
|
_ <- OptionT.withFilterF(
|
||||||
funcName = callArrow.funcName.value,
|
T.checkArgumentsNumber(
|
||||||
baseType = arrowType
|
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`)
|
||||||
)
|
)
|
||||||
)
|
} yield valueRaw
|
||||||
)(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 raw.copy(arguments = args)).value
|
||||||
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
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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))))
|
||||||
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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 {
|
||||||
|
31
utils/helpers/src/main/scala/aqua/syntax/optiont.scala
Normal file
31
utils/helpers/src/main/scala/aqua/syntax/optiont.scala
Normal 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))
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user