Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Merged
merged 8 commits into from
Nov 1, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
173 changes: 98 additions & 75 deletions semantics/src/main/scala/aqua/semantics/rules/ValuesAlgebra.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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.*
Expand Down Expand Up @@ -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
)
)
)(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

}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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))))
Expand Down
43 changes: 43 additions & 0 deletions semantics/src/test/scala/aqua/semantics/SemanticsSpec.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
}
}
}
17 changes: 14 additions & 3 deletions types/src/main/scala/aqua/types/Type.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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]

/**
Expand Down Expand Up @@ -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 {
Expand All @@ -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 {
Expand Down
31 changes: 31 additions & 0 deletions utils/helpers/src/main/scala/aqua/syntax/optiont.scala
Original file line number Diff line number Diff line change
@@ -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