diff --git a/.github/e2e/docker-compose.yml b/.github/e2e/docker-compose.yml deleted file mode 100644 index 7339b562..00000000 --- a/.github/e2e/docker-compose.yml +++ /dev/null @@ -1,134 +0,0 @@ -networks: - nox: - driver: bridge - ipam: - config: - - subnet: 10.50.10.0/24 - -services: - nox-1: - image: ${NOX_IMAGE} - ports: - - 7771:7771 - - 9991:9991 - command: - - --aqua-pool-size=2 - - -t=7771 - - -w=9991 - - -x=10.50.10.10 - - --external-maddrs - - /dns4/nox-1/tcp/7771 - - /dns4/nox-1/tcp/9991/ws - - --allow-private-ips - - --local - # - --bootstraps=/dns/nox-1/tcp/7771 - # 12D3KooWBM3SdXWqGaawQDGQ6JprtwswEg3FWGvGhmgmMez1vRbR - - -k=hK62afickoeP2uZbmSkAYXxxqP8ozq16VRN7qfTP719EHC5V5tjrtW57BSjUr8GvsEXmJRbtejUWyPZ2rZMyQdq - networks: - nox: - ipv4_address: 10.50.10.10 - - nox-2: - image: ${NOX_IMAGE} - ports: - - 7772:7772 - - 9992:9992 - command: - - --aqua-pool-size=2 - - -t=7772 - - -w=9992 - - -x=10.50.10.20 - - --external-maddrs - - /dns4/nox-2/tcp/7772 - - /dns4/nox-2/tcp/9992/ws - - --allow-private-ips - - --bootstraps=/dns/nox-1/tcp/7771 - # 12D3KooWQdpukY3p2DhDfUfDgphAqsGu5ZUrmQ4mcHSGrRag6gQK - - -k=2WijTVdhVRzyZamWjqPx4V4iNMrajegNMwNa2PmvPSZV6RRpo5M2fsPWdQr22HVRubuJhhSw8BrWiGt6FPhFAuXy - networks: - nox: - ipv4_address: 10.50.10.20 - - nox-3: - image: ${NOX_IMAGE} - ports: - - 7773:7773 - - 9993:9993 - command: - - --aqua-pool-size=2 - - -t=7773 - - -w=9993 - - -x=10.50.10.30 - - --external-maddrs - - /dns4/nox-3/tcp/7773 - - /dns4/nox-3/tcp/9993/ws - - --allow-private-ips - - --bootstraps=/dns/nox-1/tcp/7771 - # 12D3KooWRT8V5awYdEZm6aAV9HWweCEbhWd7df4wehqHZXAB7yMZ - - -k=2n2wBVanBeu2GWtvKBdrYK9DJAocgG3PrTUXMharq6TTfxqTL4sLdXL9BF23n6rsnkAY5pR9vBtx2uWYDQAiZdrX - networks: - nox: - ipv4_address: 10.50.10.30 - - nox-4: - image: ${NOX_IMAGE} - ports: - - 7774:7774 - - 9994:9994 - command: - - --aqua-pool-size=2 - - -t=7774 - - -w=9994 - - -x=10.50.10.40 - - --external-maddrs - - /dns4/nox-4/tcp/7774 - - /dns4/nox-4/tcp/9994/ws - - --allow-private-ips - - --bootstraps=/dns/nox-1/tcp/7771 - # 12D3KooWBzLSu9RL7wLP6oUowzCbkCj2AGBSXkHSJKuq4wwTfwof - - -k=4zp8ucAikkjB8CmkufYiFBW4QCDUCbQG7yMjviX7W8bMyN5rfChQ2Pi5QCWThrCTbAm9uq5nbFbxtFcNZq3De4dX - networks: - nox: - ipv4_address: 10.50.10.40 - - nox-5: - image: ${NOX_IMAGE} - ports: - - 7775:7775 - - 9995:9995 - command: - - --aqua-pool-size=2 - - -t=7775 - - -w=9995 - - -x=10.50.10.50 - - --external-maddrs - - /dns4/nox-5/tcp/7775 - - /dns4/nox-5/tcp/9995/ws - - --allow-private-ips - - --bootstraps=/dns/nox-1/tcp/7771 - # 12D3KooWBf6hFgrnXwHkBnwPGMysP3b1NJe5HGtAWPYfwmQ2MBiU - - -k=3ry26rm5gkJXvdqRH4FoM3ezWq4xVVsBQF7wtKq4E4pbuaa6p1F84tNqifUS7DdfJL9hs2gcdW64Wc342vHZHMUp - networks: - nox: - ipv4_address: 10.50.10.50 - - nox-6: - image: ${NOX_IMAGE} - ports: - - 7776:7776 - - 9996:9996 - command: - - --aqua-pool-size=2 - - -t=7776 - - -w=9996 - - --bootstraps=/dns/nox-1/tcp/7771 - - -x=10.50.10.60 - - --external-maddrs - - /dns4/nox-6/tcp/7776 - - /dns4/nox-6/tcp/9996/ws - - --allow-private-ips - # 12D3KooWPisGn7JhooWhggndz25WM7vQ2JmA121EV8jUDQ5xMovJ - - -k=5Qh8bB1sF28uLPwr3HTvEksCeC6mAWQvebCfcgv9y6j4qKwSzNKm2tzLUg4nACUEo2KZpBw11gNCnwaAdM7o1pEn - networks: - nox: - ipv4_address: 10.50.10.60 diff --git a/.github/release-please/manifest.json b/.github/release-please/manifest.json index b19a3f40..28443523 100644 --- a/.github/release-please/manifest.json +++ b/.github/release-please/manifest.json @@ -1,3 +1,3 @@ { - ".": "0.14.2" + ".": "0.14.4" } diff --git a/.github/workflows/run-tests.yml b/.github/workflows/run-tests.yml index 5eeabe7f..c29a1ce3 100644 --- a/.github/workflows/run-tests.yml +++ b/.github/workflows/run-tests.yml @@ -32,3 +32,4 @@ jobs: uses: ./.github/workflows/tests.yml with: ref: ${{ github.ref }} + nox-image: "docker.fluence.dev/nox:renovate-avm_4905_1" diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index fbac487b..aa60385e 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -22,8 +22,8 @@ on: env: FORCE_COLOR: true - NOX_IMAGE: "${{ inputs.nox-image }}" FLUENCE_ENV: "${{ inputs.fluence-env }}" + FCLI_V_NOX: "${{ inputs.nox-image }}" jobs: aqua: @@ -64,14 +64,17 @@ jobs: repository: fluencelabs/aqua ref: ${{ inputs.ref }} - - name: Pull nox image - run: docker pull docker.fluence.dev/nox:feat-vm-425-aquavm-mem-limits-from-config-2_4983_1@sha256:71bc65e096931ad1abc92b166b0033f01a718084d0f275d070ea495353da2d89 - - - name: Run nox - uses: isbang/compose-action@v1.5.1 + - name: Setup fcli + uses: fluencelabs/setup-fluence@v1 with: - compose-file: ".github/e2e/docker-compose.yml" - down-flags: "--volumes" + artifact: fcli + version: unstable + + - name: Init local env with fcli + run: fluence local init --no-input + + - name: Run nox network + run: fluence local up - name: Cache Scala uses: coursier/cache-action@v6 diff --git a/CHANGELOG.md b/CHANGELOG.md index 3bc71012..2343acc6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,21 @@ # Changelog +## [0.14.4](https://github.com/fluencelabs/aqua/compare/aqua-v0.14.3...aqua-v0.14.4) (2024-02-29) + + +### Bug Fixes + +* **compiler:** Bug in renaming [LNG-346] ([#1094](https://github.com/fluencelabs/aqua/issues/1094)) ([d7fef3d](https://github.com/fluencelabs/aqua/commit/d7fef3db5fda6c2dfeacac84c3cff2188538a35a)) +* **compiler:** Import abilities with `use` [LNG-324] ([#1077](https://github.com/fluencelabs/aqua/issues/1077)) ([a6c8e75](https://github.com/fluencelabs/aqua/commit/a6c8e75c270efd358908f285310f1f1d177a65fb)) + +## [0.14.3](https://github.com/fluencelabs/aqua/compare/aqua-v0.14.2...aqua-v0.14.3) (2024-02-29) + + +### Bug Fixes + +* **compiler:** Change `noEmptyResponse` default to `true` ([#1093](https://github.com/fluencelabs/aqua/issues/1093)) ([23aba18](https://github.com/fluencelabs/aqua/commit/23aba18c7d7fb86acd2470474f776f00a78ee9c6)) +* **language-server:** Name clashing in LSP [LNG-342] ([#1089](https://github.com/fluencelabs/aqua/issues/1089)) ([3e9d385](https://github.com/fluencelabs/aqua/commit/3e9d3856685f74a6be5d0aa288cd7e4f95010901)) + ## [0.14.2](https://github.com/fluencelabs/aqua/compare/aqua-v0.14.1...aqua-v0.14.2) (2024-02-21) diff --git a/api/api-npm/index.js b/api/api-npm/index.js index c1f7c5cc..b1581c1a 100644 --- a/api/api-npm/index.js +++ b/api/api-npm/index.js @@ -7,7 +7,7 @@ function getConfig({ noXor = false, targetType = "air", tracing = false, - noEmptyResponse = false, + noEmptyResponse = true, }) { return new AquaConfig( logLevel, diff --git a/api/api-npm/package.json b/api/api-npm/package.json index 61736777..0bc6f24e 100644 --- a/api/api-npm/package.json +++ b/api/api-npm/package.json @@ -1,6 +1,6 @@ { "name": "@fluencelabs/aqua-api", - "version": "0.14.2", + "version": "0.14.4", "description": "Aqua API", "type": "module", "main": "index.js", diff --git a/api/api/.js/src/main/scala/api/types/InputTypes.scala b/api/api/.js/src/main/scala/api/types/InputTypes.scala index 98ade0ee..aca5e9ba 100644 --- a/api/api/.js/src/main/scala/api/types/InputTypes.scala +++ b/api/api/.js/src/main/scala/api/types/InputTypes.scala @@ -72,7 +72,7 @@ object AquaConfig { noXor = cjs.noXor.getOrElse(false), noRelay = cjs.noRelay.getOrElse(false), tracing = cjs.tracing.getOrElse(false), - noEmptyResponse = cjs.noEmptyResponse.getOrElse(false) + noEmptyResponse = cjs.noEmptyResponse.getOrElse(true) ) } } diff --git a/aqua-src/antithesis.aqua b/aqua-src/antithesis.aqua index 0f3ce6a5..7d07de39 100644 --- a/aqua-src/antithesis.aqua +++ b/aqua-src/antithesis.aqua @@ -1,12 +1,48 @@ -aqua Job declares * +aqua Main -import someString from "hack" -use timeout, someString from "hack.aqua" as P +export bugLNG346 -export timeout +ability Promise: + yield() -> string -func timeout() -> string: - res <- P.timeout() - b = someString() - a = P.someString() - <- res \ No newline at end of file +func done_nil() -> string: + <- "" + +func done() -> Promise: + <- Promise(yield = done_nil) + +ability Compute: + yield() -> string + +alias WorkerYield: -> string +alias Yield: WorkerYield -> Promise + +func wait_for() -> Yield: + wait = func (cb: -> string) -> Promise: + yield = func () -> string: + e <- cb() + <- e + <- Promise(yield = yield) + <- wait + +ability Function: + run(dealId: string) -> string + +func simple{Compute}(yield: Yield) -> Function: + deal_run = func () -> string: + c_yield = func () -> string: + <- Compute.yield() + yieeld <- yield(c_yield) + res <- yieeld.yield() + <- res + <- Function(run = deal_run) + +func bugLNG346() -> string: + res: *string + yieeld = func () -> string: + res <<- "hello" + <- "" + c = Compute(yield = yieeld) + fn = simple{c}(wait_for()) + r <- fn.run("") + <- res! \ No newline at end of file diff --git a/aqua-src/declare.aqua b/aqua-src/declare.aqua index e2048ef7..5b38ae38 100644 --- a/aqua-src/declare.aqua +++ b/aqua-src/declare.aqua @@ -1,4 +1,4 @@ -module DeclareModule declares decl_foo, decl_bar, SuperFoo, DECLARE_CONST, DECLARE_CONST2 +aqua DeclareModule declares decl_foo, decl_bar, SuperFoo, DECLARE_CONST, DECLARE_CONST2 export SuperFoo const DECLARE_CONST = "declare_const" diff --git a/build.sbt b/build.sbt index b9fc041a..0bcfbbb8 100644 --- a/build.sbt +++ b/build.sbt @@ -1,6 +1,6 @@ import BundleJS.* -val aquaVersion = "0.14.2" +val aquaVersion = "0.14.4" val scalaV = "3.4.0" val catsV = "2.10.0" 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/integration-tests/aqua/examples/abilitiesClosureRename.aqua b/integration-tests/aqua/examples/abilitiesClosureRename.aqua new file mode 100644 index 00000000..6081ae9f --- /dev/null +++ b/integration-tests/aqua/examples/abilitiesClosureRename.aqua @@ -0,0 +1,48 @@ +aqua AbilitiesClosureRename + +export bugLNG346 + +ability Promise: + yield() -> string + +func done_nil() -> string: + <- "" + +func done() -> Promise: + <- Promise(yield = done_nil) + +ability Compute: + yield() -> string + +alias WorkerYield: -> string +alias Yield: WorkerYield -> Promise + +func wait_for() -> Yield: + wait = func (cb: -> string) -> Promise: + yield = func () -> string: + e <- cb() + <- e + <- Promise(yield = yield) + <- wait + +ability Function: + run(dealId: string) -> string + +func simple{Compute}(yield: Yield) -> Function: + deal_run = func () -> string: + c_yield = func () -> string: + <- Compute.yield() + yieeld <- yield(c_yield) + res <- yieeld.yield() + <- res + <- Function(run = deal_run) + +func bugLNG346() -> string: + res: *string + yieeld = func () -> string: + res <<- "hello" + <- "" + c = Compute(yield = yieeld) + fn = simple{c}(wait_for()) + r <- fn.run("") + <- res! \ No newline at end of file diff --git a/integration-tests/package.json b/integration-tests/package.json index 5e2fb506..ab3f60c2 100644 --- a/integration-tests/package.json +++ b/integration-tests/package.json @@ -29,7 +29,7 @@ }, "prettier": {}, "devDependencies": { - "@fluencelabs/aqua-api": "0.14.2", + "@fluencelabs/aqua-api": "0.14.4", "@fluencelabs/aqua-lib": "0.10.2", "@types/jest": "29.5.11", "@types/node": "18.19.10", diff --git a/integration-tests/src/__test__/examples.spec.ts b/integration-tests/src/__test__/examples.spec.ts index 4fa629d0..5d40b16a 100644 --- a/integration-tests/src/__test__/examples.spec.ts +++ b/integration-tests/src/__test__/examples.spec.ts @@ -41,6 +41,7 @@ import { returnSrvAsAbilityCall, } from "../examples/abilityCall.js"; import { bugLNG314Call, bugLNG338Call } from "../examples/abilityClosureCall.js"; +import { bugLNG346Call } from "../examples/abilityClosureRenameCall.js"; import { nilLengthCall, nilLiteralCall, @@ -670,6 +671,11 @@ describe("Testing examples", () => { expect(result).toEqual("job done"); }); + it("abilitiesClosureRename.aqua bug LNG-346", async () => { + let result = await bugLNG346Call(); + expect(result).toEqual("hello"); + }); + it("functors.aqua LNG-119 bug", async () => { let result = await bugLng119Call(); expect(result).toEqual([1]); diff --git a/integration-tests/src/examples/abilityClosureRenameCall.ts b/integration-tests/src/examples/abilityClosureRenameCall.ts new file mode 100644 index 00000000..e5769e42 --- /dev/null +++ b/integration-tests/src/examples/abilityClosureRenameCall.ts @@ -0,0 +1,8 @@ +import { + bugLNG346 +} from "../compiled/examples/abilitiesClosureRename.js"; + +export async function bugLNG346Call(): Promise { + return await bugLNG346(); +} + diff --git a/language-server/language-server-npm/package.json b/language-server/language-server-npm/package.json index 2a22ecfc..f46bd8dd 100644 --- a/language-server/language-server-npm/package.json +++ b/language-server/language-server-npm/package.json @@ -1,6 +1,6 @@ { "name": "@fluencelabs/aqua-language-server-api", - "version": "0.14.2", + "version": "0.14.4", "description": "Aqua Language Server API", "type": "commonjs", "files": [ diff --git a/model/inline/src/main/scala/aqua/model/inline/ArrowInliner.scala b/model/inline/src/main/scala/aqua/model/inline/ArrowInliner.scala index 0287394d..be3ebd27 100644 --- a/model/inline/src/main/scala/aqua/model/inline/ArrowInliner.scala +++ b/model/inline/src/main/scala/aqua/model/inline/ArrowInliner.scala @@ -507,9 +507,7 @@ object ArrowInliner extends Logging { * are prohibited because they are used inside **this function**. */ defineNames <- StateT.liftF( - fn.body.definesVarNames.map( - _ -- argNames -- capturedNames - ) + fn.body.definesVarNames ) defineRenames <- Mangler[S].findAndForbidNames(defineNames) canonStreamsWithNames <- canonStreamVariables(args) 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/pnpm-lock.yaml b/pnpm-lock.yaml index 8ec18e9f..b353c54e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -37,7 +37,7 @@ importers: version: 1.9.1 devDependencies: '@fluencelabs/aqua-api': - specifier: 0.14.2 + specifier: 0.14.4 version: link:../api/api-npm '@fluencelabs/aqua-lib': specifier: 0.10.2 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(