Skip to content

Commit

Permalink
fix(compiler): Import abilities with use [LNG-324] (#1077)
Browse files Browse the repository at this point in the history
* Add IntoApply

* Savepoint

* Add backtrack

* Return ability name

* Add service resolution

* Return import ability

* Add test

* Rewrite toDottedName

* Rewrite ability resolution

* Fix offset

* Add tests

* Add test

* Add comments

* Add test

---------

Co-authored-by: Dima <[email protected]>
  • Loading branch information
InversionSpaces and DieMyst authored Feb 29, 2024
1 parent d7fef3d commit a6c8e75
Show file tree
Hide file tree
Showing 9 changed files with 573 additions and 136 deletions.
419 changes: 400 additions & 19 deletions compiler/src/test/scala/aqua/compiler/AquaCompilerSpec.scala

Large diffs are not rendered by default.

5 changes: 5 additions & 0 deletions parser/src/main/scala/aqua/parser/lexer/NamedArg.scala
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,11 @@ enum NamedArg[F[_]] extends Token[F] {
case Full(name, value) => Full(name.mapK(fk), value.mapK(fk))
case Short(variable) => Short(variable.mapK(fk))
}

override def toString: String = this match {
case Full(name, value) => s"$name = $value"
case Short(variable) => variable.toString
}
}

object NamedArg {
Expand Down
44 changes: 39 additions & 5 deletions parser/src/main/scala/aqua/parser/lexer/PropertyOp.scala
Original file line number Diff line number Diff line change
@@ -1,20 +1,20 @@
package aqua.parser.lexer

import aqua.parser.lexer.CallArrowToken.CallBraces
import aqua.parser.lexer.NamedArg.namedArgs
import aqua.parser.lexer.Token.*
import aqua.parser.lift.LiftParser
import aqua.parser.lift.LiftParser.*
import aqua.parser.lift.Span
import aqua.parser.lift.Span.{P0ToSpan, PToSpan}
import aqua.types.LiteralType
import aqua.parser.lexer.CallArrowToken.CallBraces
import aqua.parser.lexer.NamedArg.namedArgs

import cats.~>
import cats.data.{NonEmptyList, NonEmptyMap}
import cats.parse.{Numbers, Parser as P, Parser0 as P0}
import cats.syntax.comonad.*
import cats.syntax.functor.*
import cats.{Comonad, Functor}
import cats.~>
import scala.language.postfixOps

sealed trait PropertyOp[F[_]] extends Token[F] {
Expand All @@ -36,7 +36,7 @@ case class IntoField[F[_]: Comonad](name: F[String]) extends PropertyOp[F] {

override def mapK[K[_]: Comonad](fk: F ~> K): PropertyOp[K] = copy(fk(name))

def value: String = name.extract
lazy val value: String = name.extract

override def toString: String = name.extract
}
Expand All @@ -46,6 +46,8 @@ case class IntoIndex[F[_]: Comonad](point: F[Unit], idx: Option[ValueToken[F]])
override def as[T](v: T): F[T] = point.as(v)

override def mapK[K[_]: Comonad](fk: F ~> K): IntoIndex[K] = copy(fk(point), idx.map(_.mapK(fk)))

override def toString: String = s"[$idx]"
}

case class IntoCopy[F[_]: Comonad](
Expand All @@ -56,6 +58,27 @@ case class IntoCopy[F[_]: Comonad](

override def mapK[K[_]: Comonad](fk: F ~> K): IntoCopy[K] =
copy(fk(point), args.map(_.mapK(fk)))

override def toString: String = s".copy(${args.map(_.toString).toList.mkString(", ")})"
}

/**
* WARNING: This is parsed when we have parens after a name, but `IntoArrow` failed to parse.
* This is a case of imported named type, e.g. `Some.Imported.Module.DefinedAbility(...)`
* It is transformed into `NamedTypeValue` in `ValuesAlgebra`
* TODO: Eliminate `IntoArrow`, unify it with this property
*/
case class IntoApply[F[_]: Comonad](
argsF: F[NonEmptyList[NamedArg[F]]]
) extends PropertyOp[F] {
lazy val args: NonEmptyList[NamedArg[F]] = argsF.extract

override def as[T](v: T): F[T] = argsF.as(v)

override def mapK[K[_]: Comonad](fk: F ~> K): IntoApply[K] =
copy(fk(argsF.map(_.map(_.mapK(fk)))))

override def toString: String = s"(${args.map(_.toString).toList.mkString(", ")})"
}

object PropertyOp {
Expand Down Expand Up @@ -87,8 +110,19 @@ object PropertyOp {
}
}

private val parseApply: P[PropertyOp[Span.S]] =
namedArgs.lift.map(IntoApply.apply)

private val parseOp: P[PropertyOp[Span.S]] =
P.oneOf(parseCopy.backtrack :: parseArrow.backtrack :: parseField :: parseIdx :: Nil)
P.oneOf(
// NOTE: order is important here
// intoApply has lower priority than intoArrow
parseCopy.backtrack ::
parseArrow.backtrack ::
parseField ::
parseIdx ::
parseApply.backtrack :: Nil
)

val ops: P[NonEmptyList[PropertyOp[Span.S]]] =
parseOp.rep
Expand Down
12 changes: 7 additions & 5 deletions parser/src/main/scala/aqua/parser/lexer/TypeToken.scala
Original file line number Diff line number Diff line change
Expand Up @@ -121,9 +121,11 @@ case class ArrowTypeToken[S[_]: Comonad](
args.map { case (n, t) => (n.map(_.mapK(fk)), t.mapK(fk)) },
res.map(_.mapK(fk)),
abilities.map(_.mapK(fk))
)
)
def argTypes: List[TypeToken[S]] = abilities ++ args.map(_._2)
lazy val absWithArgs: List[(Option[Name[S]], TypeToken[S])] = abilities.map(n => Some(n.asName) -> n) ++ args

lazy val absWithArgs: List[(Option[Name[S]], TypeToken[S])] =
abilities.map(n => Some(n.asName) -> n) ++ args
}

object ArrowTypeToken {
Expand All @@ -136,9 +138,9 @@ object ArrowTypeToken {
).map(_.toList)

// {SomeAb, SecondAb} for NamedTypeToken
def abilities(): P0[List[NamedTypeToken[S]]] =
(`{` *> comma(`Class`.surroundedBy(`/s*`).lift.map(NamedTypeToken(_)))
.map(_.toList) <* `}`).?.map(_.getOrElse(List.empty))
def abilities(): P0[List[NamedTypeToken[S]]] = (
`{` *> comma(NamedTypeToken.dotted).map(_.toList) <* `}`
).?.map(_.getOrElse(List.empty))

def `arrowdef`(argTypeP: P[TypeToken[Span.S]]): P[ArrowTypeToken[Span.S]] =
((abilities() ~ comma0(argTypeP)).with1 ~ ` -> `.lift ~
Expand Down
181 changes: 87 additions & 94 deletions parser/src/main/scala/aqua/parser/lexer/ValueToken.scala
Original file line number Diff line number Diff line change
Expand Up @@ -42,112 +42,105 @@ case class PropertyToken[F[_]: Comonad](
name.forall(c => !c.isLetter || c.isUpper)

/**
* This method tries to convert property token to
* property token with dotted var name inside value token.
*
* Next properties pattern is untouched:
* Class (field)*
*
* Next properties pattern is transformed:
* (Class)* (CONST | field) ..props..
* ^^^^^^^^^^^^^^^^^^^^^^^^
* this part is transformed to dotted name.
* Try to transform this token to imported ability access
* e.g. in `Some.Imported.Module.Ab.innerAb.call(...)`:
* - `Some.Imported.Module` is imported module name
* - `Ab.innerAb.call(...)` is ability access
* so it should be handled as `(Some.Imported.Module.Ab).innerAb.call(...)`
* ^^^^^^^^^^^^^^^^^^^^^^^
* ability name inside `VarToken` as one string
* but we don't know this in advance, so this method returns
* a list of all possible (imported module name, ability access value token) pairs
* so calling code can check what prefix is valid imported module name and
* handle the corresponding ability access value token.
*/
private def toDottedName: Option[ValueToken[F]] = value match {
case VarToken(name) =>
// Pattern `Class (field)*` is ability access
// and should not be transformed
val isAbility = isClass(name.value) && properties.forall {
case f @ IntoField(_) => isField(f.value)
case _ => true
}

if (isAbility) none
else {
// Gather prefix of properties that are IntoField
val props = name.value +: properties.toList.view.map {
case IntoField(name) => name.extract.some
case _ => none
}.takeWhile(_.isDefined).flatten.toList

val propsWithIndex = props.zipWithIndex

// Find first property that is not Class
val classesTill = propsWithIndex.find { case (name, _) =>
!isClass(name)
}.collect { case (_, idx) =>
idx
}.getOrElse(props.length)

// Find last property after classes
// that is CONST or field
val lastSuitable = propsWithIndex
.take(classesTill)
.findLast { case (name, _) =>
isConst(name) || isField(name)
def toAbility: List[(NamedTypeToken[F], ValueToken[F])] =
value match {
// NOTE: guard against recursion: if dot is alredy in the name, do not transform
case VarToken(name) if !name.value.contains(".") =>
val fields = properties.toList.takeWhile {
case IntoField(_) => true
case _ => false
}.collect { case f @ IntoField(_) => f.value }.toList
val names = name.value +: fields

fields.inits
// do not use the last field
// those cases are handled in `toCallArrow` and `toNamedValue`
.drop(1)
.map { init =>
// Length of the import name
val importLength = init.length + 1
// Length of the imported name
val nameLength = importLength + 1
val newProps = NonEmptyList.fromList(
properties.toList.drop(importLength)
)
val newName = name.rename(names.take(nameLength).mkString("."))
val importAbility = name.rename(names.take(importLength).mkString(".")).asTypeToken

val varToken = VarToken(newName)
val token = newProps.fold(varToken)(ps => PropertyToken(varToken, ps))

importAbility -> token
}
.collect { case (_, idx) => idx }

lastSuitable.map(last =>
val newProps = NonEmptyList.fromList(
properties.toList.drop(last + 1)
)
val newName = props.take(last + 1).mkString(".")
val varToken = VarToken(name.rename(newName))

newProps.fold(varToken)(props => PropertyToken(varToken, props))
)
}
case _ => none
}
.toList
// test shorter prefixes first
.reverse
case _ => Nil
}

/**
* This method tries to convert property token to
* call arrow token.
*
* Next properties pattern is transformed:
* (Class)+ arrow()
* ^^^^^^^
* this part is transformed to ability name.
* Try to convert this token into `CallArrowToken`
* e.g. `Some.Imported.Module.call(...)`
* ^^^^^^^^^^^^^^^^^^^^ ^^^^
* ability name function name
*/
private def toCallArrow: Option[CallArrowToken[F]] = value match {
case VarToken(name) =>
val ability = properties.init.traverse {
case f @ IntoField(_) => f.value.some
case _ => none
}.map(
name.value +: _
).filter(
_.forall(isClass)
).map(props => name.rename(props.mkString(".")))

(properties.last, ability) match {
case (IntoArrow(funcName, args), Some(ability)) =>
CallArrowToken(
ability.asTypeToken.some,
funcName,
args
).some
def toCallArrow: Option[CallArrowToken[F]] = (
value,
properties.last
) match {
case (VarToken(name), IntoArrow(funcName, args)) =>
properties.init.traverse {
case IntoField(name) => name.extract.some
case _ => none
}.map { fields =>
val imported = name
.rename(
(name.value +: fields).mkString(".")
)
.asTypeToken

CallArrowToken(
imported.some,
funcName,
args
)
}
case _ => none
}

/**
* This is a hacky method to adjust parsing result
* to format that was used previously.
* This method tries to convert property token to
* call arrow token or property token with
* dotted var name inside value token.
*
* @return Some(token) if token was adjusted, None otherwise
* Try to convert this token into `NamedValueToken`,
* e.g. `Some.Imported.Module.DefinedAbility(...)`
* ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
* type name
*/
def adjust: Option[ValueToken[F]] =
toCallArrow.orElse(toDottedName)

lazy val leadingName: Option[NamedTypeToken[F]] =
value match {
case VarToken(name) => name.asTypeToken.some
def toNamedValue: Option[NamedValueToken[F]] =
(value, properties.last) match {
case (v @ VarToken(name), IntoApply(args)) =>
properties.init.traverse {
case IntoField(name) => name.extract.some
case _ => none
}.map { props =>
val typeName = name
.rename(
(name.value +: props).mkString(".")
)
.asTypeToken

NamedValueToken(typeName, args.extract)
}
case _ => none
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ class HeaderHandler[S[_]: Comonad, C](using
.toValidNec(
error(
tkn,
s"Used module has no `module` header. Please add `module` header or use ... as ModuleName, or switch to import"
s"Used module has no `aqua` header. Please add `aqua` header or use ... as ModuleName, or switch to import"
)
)

Expand Down
32 changes: 25 additions & 7 deletions semantics/src/main/scala/aqua/semantics/rules/ValuesAlgebra.scala
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package aqua.semantics.rules

import aqua.errors.Errors.internalError
import aqua.helpers.syntax.optiont.*
import aqua.parser.lexer.*
import aqua.parser.lexer.InfixToken.{BoolOp, CmpOp, EqOp, MathOp, Op as InfOp}
Expand Down Expand Up @@ -85,6 +86,8 @@ class ValuesAlgebra[S[_], Alg[_]: Monad](using
idx <- OptionT(op.idx.fold(LiteralRaw.Zero.some.pure)(valueToRaw))
valueType <- OptionT(T.resolveIntoIndex(op, rootType, idx.`type`))
} yield IntoIndexRaw(idx, valueType)).value
case op: IntoApply[S] =>
internalError("Unexpected. `IntoApply` expected to be transformed into `NamedValueToken`")
}

def valueToRaw(v: ValueToken[S]): Alg[Option[ValueRaw]] =
Expand Down Expand Up @@ -129,14 +132,29 @@ class ValuesAlgebra[S[_], Alg[_]: Monad](using
* so here we try to differentiate them and adjust property
* token accordingly.
*/
prop.leadingName.fold(default)(name =>
A.isDefinedAbility(name)
.flatMap(isDefined =>
prop.adjust
.filter(_ => isDefined)
.fold(default)(valueToRaw)

val callArrow = OptionT
.fromOption(prop.toCallArrow)
.filterF(ca =>
ca.ability.fold(false.pure)(
A.isDefinedAbility
)
)
)
.widen[ValueToken[S]]

val ability = OptionT(
prop.toAbility.findM { case (ab, _) =>
// Test if name is an import
A.isDefinedAbility(ab)
}
).map { case (_, token) => token }

val namedValue = OptionT
.fromOption(prop.toNamedValue)
.filterF(nv => T.resolveType(nv.typeName, mustBeDefined = false).map(_.isDefined))
.widen[ValueToken[S]]

callArrow.orElse(ability).orElse(namedValue).foldF(default)(valueToRaw)

case dvt @ NamedValueToken(typeName, fields) =>
(for {
Expand Down
Loading

0 comments on commit a6c8e75

Please sign in to comment.