fix(compiler): Type check arrow calls on services and abilities [LNG-315] (#1037)

* Rewrite resolveIntoArrow

* Refactor

* Refactor resolveIntoCopy

* Refactor resolveIntoIndex

* Refactor resolveIntoField

* Fix test

* Remove package-lock.json

* Add tests

* Add comment
This commit is contained in:
InversionSpaces 2024-01-10 11:36:20 +01:00 committed by GitHub
parent 5241f522d8
commit d46ee0347f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 325 additions and 121 deletions

View File

@ -7,7 +7,7 @@ ability WorkerJob:
func disjoint_run{WorkerJob}() -> -> string:
run = func () -> string:
r <- WorkerJob.runOnSingleWorker()
r <- WorkerJob.runOnSingleWorker("worker")
<- r
<- run

View File

@ -53,29 +53,38 @@ class ValuesAlgebra[S[_], Alg[_]: Monad](using
private def resolveSingleProperty(rootType: Type, op: PropertyOp[S]): Alg[Option[PropertyRaw]] =
op match {
case op: IntoField[S] =>
T.resolveField(rootType, op)
case op: IntoArrow[S] =>
for {
maybeArgs <- op.arguments.traverse(valueToRaw)
arrowProp <- maybeArgs.sequence.flatTraverse(
T.resolveArrow(rootType, op, _)
OptionT(T.resolveIntoField(op, rootType))
.map(
_.fold(
field = t => IntoFieldRaw(op.value, t),
property = t => FunctorRaw(op.value, t)
)
)
} yield arrowProp
.value
case op: IntoArrow[S] =>
(for {
args <- op.arguments.traverse(arg => OptionT(valueToRaw(arg)))
argTypes = args.map(_.`type`)
arrowType <- OptionT(T.resolveIntoArrow(op, rootType, argTypes))
} yield IntoArrowRaw(op.name.value, arrowType, args)).value
case op: IntoCopy[S] =>
(for {
_ <- OptionT.liftF(
reportNamedArgsDuplicates(op.args)
)
fields <- op.args.traverse(arg => OptionT(valueToRaw(arg.argValue)).map(arg -> _))
prop <- OptionT(T.resolveCopy(op, rootType, fields))
} yield prop).value
case op: IntoIndex[S] =>
for {
maybeIdx <- op.idx.fold(LiteralRaw.Zero.some.pure)(valueToRaw)
idxProp <- maybeIdx.flatTraverse(
T.resolveIndex(rootType, op, _)
args <- op.args.traverse(arg =>
OptionT(valueToRaw(arg.argValue)).map(
arg.argName.value -> _
)
)
} yield idxProp
argsTypes = args.map { case (_, raw) => raw.`type` }
structType <- OptionT(T.resolveIntoCopy(op, rootType, argsTypes))
} yield IntoCopyRaw(structType, args.toNem)).value
case op: IntoIndex[S] =>
(for {
idx <- OptionT(op.idx.fold(LiteralRaw.Zero.some.pure)(valueToRaw))
valueType <- OptionT(T.resolveIntoIndex(op, rootType, idx.`type`))
} yield IntoIndexRaw(idx, valueType)).value
}
def valueToRaw(v: ValueToken[S]): Alg[Option[ValueRaw]] =

View File

@ -40,21 +40,74 @@ trait TypesAlgebra[S[_], Alg[_]] {
def defineAlias(name: NamedTypeToken[S], target: Type): Alg[Boolean]
def resolveIndex(rootT: Type, op: IntoIndex[S], idx: ValueRaw): Alg[Option[PropertyRaw]]
def resolveCopy(
token: IntoCopy[S],
/**
* Resolve `IntoIndex` property on value with `rootT` type
*
* @param op property to resolve
* @param rootT type of the value to which property is applied
* @param idxType type of the index
* @return type of the value at given index if property application is valid
*/
def resolveIntoIndex(
op: IntoIndex[S],
rootT: Type,
fields: NonEmptyList[(NamedArg[S], ValueRaw)]
): Alg[Option[PropertyRaw]]
idxType: Type
): Alg[Option[DataType]]
def resolveField(rootT: Type, op: IntoField[S]): Alg[Option[PropertyRaw]]
def resolveArrow(
/**
* Resolve `IntoCopy` property on value with `rootT` type
*
* @param op property to resolve
* @param rootT type of the value to which property is applied
* @param types types of arguments passed
* @return struct type if property application is valid
* @note `types` should correspond to `op.args`
*/
def resolveIntoCopy(
op: IntoCopy[S],
rootT: Type,
types: NonEmptyList[Type]
): Alg[Option[StructType]]
enum IntoFieldRes(`type`: Type) {
case Field(`type`: Type) extends IntoFieldRes(`type`)
case Property(`type`: Type) extends IntoFieldRes(`type`)
def fold[A](field: Type => A, property: Type => A): A =
this match {
case Field(t) => field(t)
case Property(t) => property(t)
}
}
/**
* Resolve `IntoField` property on value with `rootT` type
*
* @param op property to resolve
* @param rootT type of the value to which property is applied
* @return if property application is valid, return
* Field(type) if it's a field of rootT (fields of structs or abilities),
* Property(type) if it's a property of rootT (functors of collections)
*/
def resolveIntoField(
op: IntoField[S],
rootT: Type
): Alg[Option[IntoFieldRes]]
/**
* Resolve `IntoArrow` property on value with `rootT` type
*
* @param op property to resolve
* @param rootT type of the value to which property is applied
* @param types types of arguments passed
* @return arrow type if property application is valid
* @note `types` should correspond to `op.arguments`
*/
def resolveIntoArrow(
op: IntoArrow[S],
arguments: List[ValueRaw]
): Alg[Option[PropertyRaw]]
rootT: Type,
types: List[Type]
): Alg[Option[ArrowType]]
def ensureValuesComparable(token: Token[S], left: Type, right: Type): Alg[Boolean]

View File

@ -1,5 +1,6 @@
package aqua.semantics.rules.types
import aqua.errors.Errors.internalError
import aqua.parser.lexer.*
import aqua.raw.value.*
import aqua.semantics.Levenshtein
@ -17,6 +18,7 @@ import cats.syntax.apply.*
import cats.syntax.flatMap.*
import cats.syntax.foldable.*
import cats.syntax.functor.*
import cats.syntax.monad.*
import cats.syntax.option.*
import cats.syntax.traverse.*
import cats.{Applicative, ~>}
@ -187,132 +189,177 @@ class TypesInterpreter[S[_], X](using
.as(true)
}
override def resolveField(rootT: Type, op: IntoField[S]): State[X, Option[PropertyRaw]] = {
override def resolveIntoField(
op: IntoField[S],
rootT: Type
): State[X, Option[IntoFieldRes]] = {
rootT match {
case nt: NamedType =>
nt.fields(op.value)
.fold(
nt.fields(op.value) match {
case Some(t) =>
locations
.pointFieldLocation(nt.name, op.value, op)
.as(Some(IntoFieldRes.Field(t)))
case None =>
val fields = nt.fields.keys.map(k => s"`$k`").toList.mkString(", ")
report
.error(
op,
s"Field `${op.value}` not found in type `${nt.name}`, available: ${nt.fields.toNel.toList.map(_._1).mkString(", ")}"
s"Field `${op.value}` not found in type `${nt.name}`, available: $fields"
)
.as(None)
) { t =>
locations.pointFieldLocation(nt.name, op.value, op).as(Some(IntoFieldRaw(op.value, t)))
}
}
case t =>
t.properties
.get(op.value)
.fold(
.get(op.value) match {
case Some(t) =>
State.pure(Some(IntoFieldRes.Property(t)))
case None =>
report
.error(
op,
s"Expected data type to resolve a field '${op.value}' or a type with this property. Got: $rootT"
s"Property `${op.value}` not found in type `$t`"
)
.as(None)
)(t => State.pure(Some(FunctorRaw(op.value, t))))
}
}
}
override def resolveArrow(
rootT: Type,
override def resolveIntoArrow(
op: IntoArrow[S],
arguments: List[ValueRaw]
): State[X, Option[PropertyRaw]] = {
rootT: Type,
types: List[Type]
): State[X, Option[ArrowType]] = {
/* Safeguard to check condition on arguments */
if (op.arguments.length != types.length)
internalError(s"Invalid arguments, lists do not match: ${op.arguments} and $types")
val opName = op.name.value
rootT match {
case ab: GeneralAbilityType =>
val name = ab.name
val fields = ab.fields
lazy val fieldNames = fields.toNel.toList.map(_._1).mkString(", ")
fields(op.name.value)
.fold(
report
.error(
op,
s"Arrow `${op.name.value}` not found in type `$name`, " +
s"available: $fieldNames"
)
.as(None)
) {
case at @ ArrowType(_, _) =>
locations
.pointFieldLocation(name, op.name.value, op)
.as(Some(IntoArrowRaw(op.name.value, at, arguments)))
case _ =>
val abName = ab.fullName
ab.fields.lookup(opName) match {
case Some(at: ArrowType) =>
val reportNotEnoughArguments =
/* Report at position of arrow application */
report
.error(
op,
s"Unexpected. `${op.name.value}` must be an arrow."
s"Not enough arguments for arrow `$opName` in `$abName`, " +
s"expected: ${at.domain.length}, given: ${op.arguments.length}"
)
.as(None)
}
case t =>
t.properties
.get(op.name.value)
.fold(
report
.error(
op,
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))))
.whenA(op.arguments.length < at.domain.length)
val reportTooManyArguments =
/* Report once at position of the first extra argument */
op.arguments.drop(at.domain.length).headOption.traverse_ { arg =>
report
.error(
arg,
s"Too many arguments for arrow `$opName` in `$abName`, " +
s"expected: ${at.domain.length}, given: ${op.arguments.length}"
)
}
val checkArgumentTypes =
op.arguments
.zip(types)
.zip(at.domain.toList)
.forallM { case ((arg, argType), expectedType) =>
ensureTypeMatches(arg, expectedType, argType)
}
locations.pointFieldLocation(abName, opName, op) *>
reportNotEnoughArguments *>
reportTooManyArguments *>
checkArgumentTypes.map(typesMatch =>
Option.when(
typesMatch && at.domain.length == op.arguments.length
)(at)
)
case Some(t) =>
report
.error(op, s"Field `$opName` has non arrow type `$t` in `$abName`")
.as(None)
case None =>
val available = ab.arrowFields.keys.map(k => s"`$k`").mkString(", ")
report
.error(op, s"Arrow `$opName` not found in `$abName`, available: $available")
.as(None)
}
case t =>
/* NOTE: Arrows are only supported on services and abilities,
(`.copy(...)` for structs is resolved by separate method) */
report
.error(op, s"Arrow `$opName` not found in `$t`")
.as(None)
}
}
// TODO actually it's stateless, exists there just for reporting needs
override def resolveCopy(
token: IntoCopy[S],
override def resolveIntoCopy(
op: IntoCopy[S],
rootT: Type,
args: NonEmptyList[(NamedArg[S], ValueRaw)]
): State[X, Option[PropertyRaw]] =
types: NonEmptyList[Type]
): State[X, Option[StructType]] = {
if (op.args.length != types.length)
internalError(s"Invalid arguments, lists do not match: ${op.args} and $types")
rootT match {
case st: StructType =>
args.forallM { case (arg, value) =>
val fieldName = arg.argName.value
st.fields.lookup(fieldName) match {
case Some(t) =>
ensureTypeMatches(arg.argValue, t, value.`type`)
case None =>
report.error(arg.argName, s"No field with name '$fieldName' in $rootT").as(false)
op.args
.zip(types)
.forallM { case (arg, argType) =>
val fieldName = arg.argName.value
st.fields.lookup(fieldName) match {
case Some(fieldType) =>
ensureTypeMatches(arg.argValue, fieldType, argType)
case None =>
report
.error(
arg.argName,
s"No field with name '$fieldName' in `$st`"
)
.as(false)
}
}
}.map(
Option.when(_)(
IntoCopyRaw(
st,
args.map { case (arg, value) =>
arg.argName.value -> value
}.toNem
)
.map(Option.when(_)(st))
case t =>
report
.error(
op,
s"Non data type `$t` does not support `.copy`"
)
)
case _ =>
report.error(token, s"Expected $rootT to be a data type").as(None)
.as(None)
}
}
// TODO actually it's stateless, exists there just for reporting needs
override def resolveIndex(
rootT: Type,
override def resolveIntoIndex(
op: IntoIndex[S],
idx: ValueRaw
): State[X, Option[PropertyRaw]] =
if (!ScalarType.i64.acceptsValueOf(idx.`type`))
report.error(op, s"Expected numeric index, got $idx").as(None)
else
rootT match {
case ot: OptionType =>
op.idx.fold(
State.pure(Some(IntoIndexRaw(idx, ot.element)))
)(v => report.error(v, s"Options might have only one element, use ! to get it").as(None))
case rt: CollectionType =>
State.pure(Some(IntoIndexRaw(idx, rt.element)))
case _ =>
report.error(op, s"Expected $rootT to be a collection type").as(None)
}
rootT: Type,
idxType: Type
): State[X, Option[DataType]] =
ensureTypeOneOf(
op.idx.getOrElse(op),
ScalarType.integer,
idxType
) *> (rootT match {
case ot: OptionType =>
op.idx.fold(State.pure(Some(ot.element)))(v =>
// TODO: Is this a right place to report this error?
// It is not a type error, but rather a syntax error
report.error(v, s"Options might have only one element, use ! to get it").as(None)
)
case rt: CollectionType =>
State.pure(Some(rt.element))
case t =>
report
.error(
op,
s"Non collection type `$t` does not support indexing"
)
.as(None)
})
override def ensureValuesComparable(
token: Token[S],
@ -423,7 +470,7 @@ class TypesInterpreter[S[_], X](using
): State[X, Boolean] = for {
/* Check that required fields are present
among arguments and have correct types */
enough <- expected.fields.toNel.traverse { case (name, typ) =>
enough <- expected.fields.toNel.forallM { case (name, typ) =>
arguments.lookup(name) match {
case Some(arg -> givenType) =>
ensureTypeMatches(arg.argValue, typ, givenType)
@ -435,7 +482,7 @@ class TypesInterpreter[S[_], X](using
)
.as(false)
}
}.map(_.forall(identity))
}
expectedKeys = expected.fields.keys.toNonEmptyList
/* Report unexpected arguments */
_ <- arguments.toNel.traverse_ { case (name, arg -> typ) =>

View File

@ -65,12 +65,30 @@ class ValuesAlgebraSpec extends AnyFlatSpec with Matchers with Inside {
def stream(values: ValueToken[Id]*): CollectionToken[Id] =
CollectionToken[Id](CollectionToken.Mode.StreamMode, values.toList)
def serviceCall(
srv: String,
method: String,
args: List[ValueToken[Id]] = Nil
): PropertyToken[Id] =
PropertyToken(
variable(srv),
NonEmptyList.of(
IntoArrow(
Name[Id](method),
args
)
)
)
def allPairs[A](list: List[A]): List[(A, A)] = for {
a <- list
b <- list
} yield (a, b)
def genState(vars: Map[String, Type] = Map.empty) = {
def genState(
vars: Map[String, Type] = Map.empty,
types: Map[String, Type] = Map.empty
) = {
val init = RawContext.blank.copy(
parts = Chain
.fromSeq(ConstantRaw.defaultConstants())
@ -88,6 +106,10 @@ class ValuesAlgebraSpec extends AnyFlatSpec with Matchers with Inside {
) :: _
)
)
.focus(_.types)
.modify(types.foldLeft(_) { case (st, (name, t)) =>
st.defineType(NamedTypeToken(name), t)
})
}
def valueOfType(t: Type)(
@ -626,4 +648,72 @@ class ValuesAlgebraSpec extends AnyFlatSpec with Matchers with Inside {
value.`type` shouldBe OptionType(BottomType)
}
}
it should "type check service calls" in {
val srvName = "TestSrv"
val methodName = "testMethod"
val methodType = ArrowType(
ProductType(ScalarType.i8 :: ScalarType.string :: Nil),
ProductType(Nil)
)
def test(args: List[ValueToken[Id]], vars: Map[String, Type] = Map.empty) = {
val state = genState(
vars,
types = Map(
srvName -> ServiceType(
srvName,
NonEmptyMap.of(
methodName -> methodType
)
)
)
)
val call = serviceCall(srvName, methodName, args)
val alg = algebra()
val (st, res) = alg
.valueToRaw(call)
.run(state)
.value
res shouldBe None
atLeast(1, st.errors.toList) shouldBe a[RulesViolated[Id]]
}
// not enough arguments
// TestSrv.testMethod()
test(List.empty)
// TestSrv.testMethod(42)
test(literal("42", LiteralType.unsigned) :: Nil)
// TestSrv.testMethod(var)
test(variable("var") :: Nil, Map("var" -> ScalarType.i8))
// wrong argument type
// TestSrv.testMethod([42, var])
test(
array(literal("42", LiteralType.unsigned), variable("var")) :: Nil,
Map("var" -> ScalarType.i8)
)
// TestSrv.testMethod(42, var)
test(
literal("42", LiteralType.unsigned) :: variable("var") :: Nil,
Map("var" -> ScalarType.i64)
)
// TestSrv.testMethod("test", var)
test(
literal("test", LiteralType.string) :: variable("var") :: Nil,
Map("var" -> ScalarType.string)
)
// too many arguments
// TestSrv.testMethod(42, "test", var)
test(
literal("42", LiteralType.unsigned) ::
literal("test", LiteralType.string) ::
variable("var") :: Nil,
Map("var" -> ScalarType.string)
)
}
}

View File

@ -370,6 +370,11 @@ sealed trait NamedType extends Type {
def fields: NonEmptyMap[String, Type]
def arrowFields: Map[String, ArrowType] =
fields.toSortedMap.collect { case (name, at: ArrowType) =>
name -> at
}
/**
* Get all fields defined in this type and its fields of named type.
* Paths to fields are returned **without** type name