From a6c8e75c270efd358908f285310f1f1d177a65fb Mon Sep 17 00:00:00 2001 From: InversionSpaces Date: Thu, 29 Feb 2024 14:56:05 +0100 Subject: [PATCH] fix(compiler): Import abilities with `use` [LNG-324] (#1077) * 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 --- .../aqua/compiler/AquaCompilerSpec.scala | 419 +++++++++++++++++- .../scala/aqua/parser/lexer/NamedArg.scala | 5 + .../scala/aqua/parser/lexer/PropertyOp.scala | 44 +- .../scala/aqua/parser/lexer/TypeToken.scala | 12 +- .../scala/aqua/parser/lexer/ValueToken.scala | 173 ++++---- .../aqua/semantics/header/HeaderHandler.scala | 2 +- .../aqua/semantics/rules/ValuesAlgebra.scala | 32 +- .../semantics/rules/types/TypesAlgebra.scala | 2 +- .../rules/types/TypesInterpreter.scala | 12 +- 9 files changed, 569 insertions(+), 132 deletions(-) diff --git a/compiler/src/test/scala/aqua/compiler/AquaCompilerSpec.scala b/compiler/src/test/scala/aqua/compiler/AquaCompilerSpec.scala index a46600e0..b3727b7a 100644 --- a/compiler/src/test/scala/aqua/compiler/AquaCompilerSpec.scala +++ b/compiler/src/test/scala/aqua/compiler/AquaCompilerSpec.scala @@ -87,6 +87,25 @@ class AquaCompilerSpec extends AnyFlatSpec with Matchers with Inside { inside(funcs)(test) ) + def through(peer: ValueModel) = + MakeRes.hop(peer) + + val relay = VarRaw("-relay-", ScalarType.string) + + def getDataSrv(name: String, varName: String, t: Type) = { + CallServiceRes( + LiteralModel.quote("getDataSrv"), + name, + CallRes(Nil, Some(CallModel.Export(varName, t))), + LiteralModel.fromRaw(ValueRaw.InitPeerId) + ).leaf + } + + private val init = LiteralModel.fromRaw(ValueRaw.InitPeerId) + + private def join(vm: VarModel, size: ValueModel) = + ResBuilder.join(vm, size, init) + "aqua compiler" should "compile a simple snippet to the right context" in { val src = Map( @@ -115,25 +134,6 @@ class AquaCompilerSpec extends AnyFlatSpec with Matchers with Inside { } } - def through(peer: ValueModel) = - MakeRes.hop(peer) - - val relay = VarRaw("-relay-", ScalarType.string) - - def getDataSrv(name: String, varName: String, t: Type) = { - CallServiceRes( - LiteralModel.quote("getDataSrv"), - name, - CallRes(Nil, Some(CallModel.Export(varName, t))), - LiteralModel.fromRaw(ValueRaw.InitPeerId) - ).leaf - } - - private val init = LiteralModel.fromRaw(ValueRaw.InitPeerId) - - private def join(vm: VarModel, size: ValueModel) = - ResBuilder.join(vm, size, init) - it should "create right topology" in { val src = Map( "index.aqua" -> @@ -502,4 +502,385 @@ class AquaCompilerSpec extends AnyFlatSpec with Matchers with Inside { main.body.equalsOrShowDiff(expected) should be(true) } } + + val moduleNames = List("Test", "Imp", "Sub", "Path").inits + .takeWhile(_.nonEmpty) + .map(_.mkString(".")) + .toList + + it should "import function with `use`" in { + def test(name: String, rename: Option[String]) = { + val src = Map( + "main.aqua" -> + s"""aqua Main + |export main + |use "import.aqua"${rename.fold("")(" as " + _)} + |func main() -> i32: + | <- ${rename.getOrElse(name)}.foo() + |""".stripMargin + ) + val imports = Map( + "import.aqua" -> + s"""aqua $name declares foo + |func foo() -> i32: + | <- 42 + |""".stripMargin + ) + + val transformCfg = TransformConfig(relayVarName = None) + + insideRes(src, imports, transformCfg)( + "main" + ) { case main :: _ => + val expected = XorRes.wrap( + respCall(transformCfg, LiteralModel.number(42), initPeer), + errorCall(transformCfg, 0, initPeer) + ) + + main.body.equalsOrShowDiff(expected) should be(true) + } + } + + moduleNames.foreach { name => + val rename = "Imported" + + withClue(s"Testing $name") { + test(name, None) + } + withClue(s"Testing $name as $rename") { + test(name, rename.some) + } + } + } + + it should "import service with `use`" in { + def test(name: String, rename: Option[String]) = { + val srvName = rename.getOrElse(name) + ".Srv" + val src = Map( + "main.aqua" -> + s"""aqua Main + |export main + |use "import.aqua"${rename.fold("")(" as " + _)} + | + |func main() -> i32: + | a <- $srvName.call() + | $srvName "res-id" + | b <- $srvName.call() + | <- a + b + |""".stripMargin + ) + val imports = Map( + "import.aqua" -> + s"""aqua $name declares * + |service Srv("def-id"): + | call() -> i32 + |""".stripMargin + ) + + val transformCfg = TransformConfig(relayVarName = None) + + insideRes(src, imports, transformCfg)( + "main" + ) { case main :: _ => + def call(id: String, exp: CallModel.Export) = + CallServiceRes( + LiteralModel.quote(id), + "call", + CallRes(Nil, Some(exp)), + initPeer + ).leaf + + val a = CallModel.Export("ret", ScalarType.i32) + val b = CallModel.Export("ret-0", ScalarType.i32) + val add = CallModel.Export("add", ScalarType.i32) + val expected = XorRes.wrap( + SeqRes.wrap( + call("def-id", a), + call("res-id", b), + CallServiceRes( + LiteralModel.quote("math"), + "add", + CallRes(List(a.asVar, b.asVar), Some(add)), + initPeer + ).leaf, + respCall(transformCfg, add.asVar, initPeer) + ), + errorCall(transformCfg, 0, initPeer) + ) + + main.body.equalsOrShowDiff(expected) should be(true) + } + } + + moduleNames.foreach { name => + val rename = "Imported" + + withClue(s"Testing $name") { + test(name, None) + } + withClue(s"Testing $name as $rename") { + test(name, rename.some) + } + } + } + + it should "import ability with `use`" in { + def test(name: String, rename: Option[String]) = { + val abName = rename.getOrElse(name) + ".Ab" + val src = Map( + "main.aqua" -> + s"""aqua Main + |export main + |use "import.aqua"${rename.fold("")(" as " + _)} + |func useAb{$abName}() -> i32: + | <- $abName.a + | + |func main() -> i32: + | ab = $abName(a = 42) + | <- useAb{ab}() + |""".stripMargin + ) + val imports = Map( + "import.aqua" -> + s"""aqua $name declares * + |ability Ab: + | a: i32 + |""".stripMargin + ) + + val transformCfg = TransformConfig(relayVarName = None) + + insideRes(src, imports, transformCfg)( + "main" + ) { case main :: _ => + val ap = CallModel.Export("literal_ap", LiteralType.unsigned) + val props = ap.copy(name = "literal_props") + val expected = XorRes.wrap( + SeqRes.wrap( + // NOTE: Result of compilation is inefficient + ApRes(LiteralModel.number(42), ap).leaf, + ApRes(ap.asVar, props).leaf, + respCall(transformCfg, props.asVar, initPeer) + ), + errorCall(transformCfg, 0, initPeer) + ) + + main.body.equalsOrShowDiff(expected) should be(true) + } + } + + moduleNames.foreach { name => + val rename = "Imported" + + withClue(s"Testing $name") { + test(name, None) + } + withClue(s"Testing $name as $rename") { + test(name, rename.some) + } + } + } + + it should "import ability (nested) with `use`" in { + def test(name: String, rename: Option[String]) = { + val impName = rename.getOrElse(name) + val abName = impName + ".Ab" + val src = Map( + "main.aqua" -> + s"""aqua Main + |export main + |use "import.aqua"${rename.fold("")(" as " + _)} + |func useAb{$abName}() -> i32: + | <- $abName.ab1.ab0.call($abName.ab1.ab0.a) + | + |func main() -> i32: + | id = (x: i32) -> i32: + | <- x + | ab0 = $impName.Ab0(a = 42, call = id) + | ab1 = $impName.Ab1(ab0 = ab0) + | ab = $abName(ab1 = ab1) + | <- useAb{ab}() + |""".stripMargin + ) + val imports = Map( + "import.aqua" -> + s"""aqua $name declares * + |ability Ab0: + | a: i32 + | call(x: i32) -> i32 + | + |ability Ab1: + | ab0: Ab0 + | + |ability Ab: + | ab1: Ab1 + |""".stripMargin + ) + + val transformCfg = TransformConfig(relayVarName = None) + + insideRes(src, imports, transformCfg)( + "main" + ) { case main :: _ => + val ap = CallModel.Export("literal_ap", LiteralType.unsigned) + val props = ap.copy(name = "literal_props") + val expected = XorRes.wrap( + SeqRes.wrap( + // NOTE: Result of compilation is inefficient + ApRes(LiteralModel.number(42), ap).leaf, + ApRes(ap.asVar, props).leaf, + respCall(transformCfg, props.asVar, initPeer) + ), + errorCall(transformCfg, 0, initPeer) + ) + + main.body.equalsOrShowDiff(expected) should be(true) + } + } + + moduleNames.foreach { name => + val rename = "Imported" + + withClue(s"Testing $name") { + test(name, None) + } + withClue(s"Testing $name as $rename") { + test(name, rename.some) + } + } + } + + it should "import abilities in chain of imports" in { + case class Imp(header: String, rename: Option[String] = None) { + val as = rename.fold("")(" as " + _) + val name = rename.getOrElse(header) + + override def toString: String = s"$header$as" + } + + def test(hierarchy: List[Imp]) = { + hierarchy.nonEmpty should be(true) + + def genImp(header: String, imported: Imp, path: String): String = { + s"""aqua ${header} declares * + |use "${path}"${imported.as} + | + |ability Inner: + | ab: ${imported.name}.Outer + | + |ability Outer: + | ab: Inner + | + |func create{${imported.name}.Outer}() -> Inner: + | ab = Inner(ab = ${imported.name}.Outer) + | <- ab + |""".stripMargin + } + + val base = Imp("Base") + val basePath = "base.aqua" + val baseCode = s"""aqua Base declares * + | + |ability Outer: + | a: i32 + | call(x: i32) -> i32 + |""".stripMargin + + val withPaths = hierarchy.zipWithIndex.map { case (imp, idx) => + imp -> s"import$idx.aqua" + } + val nexts = withPaths.tail :+ (base -> basePath) + val imports = withPaths + .zip(nexts) + .map { case ((curImp, curPath), (nextImp, nextPath)) => + curPath -> genImp(curImp.header, nextImp, nextPath) + } + .appended(basePath -> baseCode) + .toMap + + val importStmts = withPaths.map { case (imp, path) => + s"use \"${path}\"${imp.as}" + }.prepended(s"use \"${basePath}\"") + val createStmts = hierarchy.reverse.zipWithIndex.flatMap { case (imp, idx) => + s"abIn${idx + 1} = ${imp.name}.create{abOut$idx}()" :: + s"abOut${idx + 1} = ${imp.name}.Outer(ab = abIn${idx + 1})" :: + Nil + }.prepended("abOut0 = Base.Outer(a = 42, call = id)") + + val lastAb = s"abOut${hierarchy.size}" + ".ab".repeat(hierarchy.size * 2) + val main = s"""aqua Main + |export main + |${importStmts.mkString("\n")} + | + |func main() -> i32: + | id = (x: i32) -> i32: + | <- x + | ${createStmts.mkString("\n ")} + | <- $lastAb.call($lastAb.a) + |""".stripMargin + + val src = Map( + "main.aqua" -> main + ) + + val transformCfg = TransformConfig(relayVarName = None) + + insideRes(src, imports, transformCfg)( + "main" + ) { case main :: _ => + val ap = CallModel.Export("literal_ap", LiteralType.unsigned) + val props = ap.copy(name = "literal_props") + val expected = XorRes.wrap( + SeqRes.wrap( + // NOTE: Result of compilation is inefficient + ApRes(LiteralModel.number(42), ap).leaf, + ApRes(ap.asVar, props).leaf, + respCall(transformCfg, props.asVar, initPeer) + ), + errorCall(transformCfg, 0, initPeer) + ) + + main.body.equalsOrShowDiff(expected) should be(true) + } + } + + // Simple + (1 to 10).map(i => (1 to i).map(n => Imp(s"Imp$n")).toList).foreach { h => + withClue(s"Testing ${h.mkString(" -> ")}") { + test(h) + } + } + + // With renaming + (1 to 10) + .map(i => + (1 to i) + .map(n => + // Rename every second one + Imp(s"Imp$n", s"Renamed$n".some.filter(_ => n % 2 == 0)) + ) + .toList + ) + .foreach { h => + withClue(s"Testing ${h.mkString(" -> ")}") { + test(h) + } + } + + // With subpath + (1 to 10) + .map(i => + (1 to i) + .map(n => s"Imp$n") + .inits + .takeWhile(_.nonEmpty) + .map(p => Imp(p.mkString("."))) + .toList + ) + .foreach { h => + withClue(s"Testing ${h.mkString(" -> ")}") { + test(h) + } + } + } } diff --git a/parser/src/main/scala/aqua/parser/lexer/NamedArg.scala b/parser/src/main/scala/aqua/parser/lexer/NamedArg.scala index c0d50dc1..8891d812 100644 --- a/parser/src/main/scala/aqua/parser/lexer/NamedArg.scala +++ b/parser/src/main/scala/aqua/parser/lexer/NamedArg.scala @@ -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 { diff --git a/parser/src/main/scala/aqua/parser/lexer/PropertyOp.scala b/parser/src/main/scala/aqua/parser/lexer/PropertyOp.scala index 162b6158..049b87bb 100644 --- a/parser/src/main/scala/aqua/parser/lexer/PropertyOp.scala +++ b/parser/src/main/scala/aqua/parser/lexer/PropertyOp.scala @@ -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] { @@ -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 } @@ -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]( @@ -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 { @@ -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 diff --git a/parser/src/main/scala/aqua/parser/lexer/TypeToken.scala b/parser/src/main/scala/aqua/parser/lexer/TypeToken.scala index 743960d1..f9e7f2f6 100644 --- a/parser/src/main/scala/aqua/parser/lexer/TypeToken.scala +++ b/parser/src/main/scala/aqua/parser/lexer/TypeToken.scala @@ -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 { @@ -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 ~ diff --git a/parser/src/main/scala/aqua/parser/lexer/ValueToken.scala b/parser/src/main/scala/aqua/parser/lexer/ValueToken.scala index bbe05c4b..6fdbfcb1 100644 --- a/parser/src/main/scala/aqua/parser/lexer/ValueToken.scala +++ b/parser/src/main/scala/aqua/parser/lexer/ValueToken.scala @@ -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 - } + 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 - 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 + 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 propsWithIndex = props.zipWithIndex + val varToken = VarToken(newName) + val token = newProps.fold(varToken)(ps => PropertyToken(varToken, ps)) - // 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) + importAbility -> token } - .collect { case (_, idx) => idx } + .toList + // test shorter prefixes first + .reverse + case _ => Nil + } - lastSuitable.map(last => - val newProps = NonEmptyList.fromList( - properties.toList.drop(last + 1) + /** + * Try to convert this token into `CallArrowToken` + * e.g. `Some.Imported.Module.call(...)` + * ^^^^^^^^^^^^^^^^^^^^ ^^^^ + * ability name function name + */ + 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(".") ) - val newName = props.take(last + 1).mkString(".") - val varToken = VarToken(name.rename(newName)) + .asTypeToken - newProps.fold(varToken)(props => PropertyToken(varToken, props)) + CallArrowToken( + imported.some, + funcName, + args ) } case _ => none } /** - * 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 `NamedValueToken`, + * e.g. `Some.Imported.Module.DefinedAbility(...)` + * ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + * type 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("."))) + 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 - (properties.last, ability) match { - case (IntoArrow(funcName, args), Some(ability)) => - CallArrowToken( - ability.asTypeToken.some, - funcName, - args - ).some - case _ => none - } - 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 - */ - def adjust: Option[ValueToken[F]] = - toCallArrow.orElse(toDottedName) - - lazy val leadingName: Option[NamedTypeToken[F]] = - value match { - case VarToken(name) => name.asTypeToken.some + NamedValueToken(typeName, args.extract) + } case _ => none } } diff --git a/semantics/src/main/scala/aqua/semantics/header/HeaderHandler.scala b/semantics/src/main/scala/aqua/semantics/header/HeaderHandler.scala index 9a32e597..21e74076 100644 --- a/semantics/src/main/scala/aqua/semantics/header/HeaderHandler.scala +++ b/semantics/src/main/scala/aqua/semantics/header/HeaderHandler.scala @@ -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" ) ) diff --git a/semantics/src/main/scala/aqua/semantics/rules/ValuesAlgebra.scala b/semantics/src/main/scala/aqua/semantics/rules/ValuesAlgebra.scala index 505778bf..86c67c87 100644 --- a/semantics/src/main/scala/aqua/semantics/rules/ValuesAlgebra.scala +++ b/semantics/src/main/scala/aqua/semantics/rules/ValuesAlgebra.scala @@ -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} @@ -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]] = @@ -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 { diff --git a/semantics/src/main/scala/aqua/semantics/rules/types/TypesAlgebra.scala b/semantics/src/main/scala/aqua/semantics/rules/types/TypesAlgebra.scala index 3d77b8a9..2dffc78b 100644 --- a/semantics/src/main/scala/aqua/semantics/rules/types/TypesAlgebra.scala +++ b/semantics/src/main/scala/aqua/semantics/rules/types/TypesAlgebra.scala @@ -19,7 +19,7 @@ trait TypesAlgebra[S[_], Alg[_]] { def resolveArrowDef(arrowDef: ArrowTypeToken[S]): Alg[Option[ArrowType]] - def resolveServiceType(name: NamedTypeToken[S]): Alg[Option[ServiceType]] + def resolveServiceType(name: NamedTypeToken[S], mustBeDefined: Boolean = true): Alg[Option[ServiceType]] def defineAbilityType( name: NamedTypeToken[S], diff --git a/semantics/src/main/scala/aqua/semantics/rules/types/TypesInterpreter.scala b/semantics/src/main/scala/aqua/semantics/rules/types/TypesInterpreter.scala index fdfcbbcd..3fccd37e 100644 --- a/semantics/src/main/scala/aqua/semantics/rules/types/TypesInterpreter.scala +++ b/semantics/src/main/scala/aqua/semantics/rules/types/TypesInterpreter.scala @@ -76,14 +76,18 @@ class TypesInterpreter[S[_], X](using }.as(none) } - override def resolveServiceType(name: NamedTypeToken[S]): State[X, Option[ServiceType]] = - resolveType(name).flatMap { + override def resolveServiceType( + name: NamedTypeToken[S], + mustBeDefined: Boolean = true + ): State[X, Option[ServiceType]] = + resolveType(name, mustBeDefined).flatMap { case Some(serviceType: ServiceType) => serviceType.some.pure - case Some(t) => + case Some(t) if mustBeDefined => report.error(name, s"Type `$t` is not a service").as(none) - case None => + case None if mustBeDefined => report.error(name, s"Type `${name.value}` is not defined").as(none) + case _ => none.pure } override def defineAbilityType(