From ae32f8027729bfd463cddc57f857c307e1e3c709 Mon Sep 17 00:00:00 2001 From: InversionSpaces Date: Tue, 9 Jan 2024 12:48:02 +0100 Subject: [PATCH] feat(compiler): `for ... rec` [LNG-307] (#1026) * Add parser * Add semantics * Add inlining * Add range test * Rewrite to for ... rec * Rewrite tests * Fix import * Add nested test * Remove only * Add yes|no test * Add multi rec test * Add pipeline test * Unignore tests * Change timeouts * Add remote rec test * Fix integration tests * Add parser test * Add semantics test * Add inlining test * Add comment --- .../aqua/examples/recursiveStreams.aqua | 13 -- .../examples/recursiveStreams/multiRec.aqua | 22 +++ .../examples/recursiveStreams/nested.aqua | 19 +++ .../examples/recursiveStreams/pipeline.aqua | 29 ++++ .../aqua/examples/recursiveStreams/range.aqua | 18 +++ .../examples/recursiveStreams/remoteRec.aqua | 19 +++ .../aqua/examples/recursiveStreams/yesNo.aqua | 21 +++ .../src/__test__/examples.spec.ts | 127 ++++++++++++++---- .../recursiveStreams/multiRecStreamCall.ts | 14 ++ .../examples/recursiveStreams/nestedCall.ts | 5 + .../examples/recursiveStreams/pipelineCall.ts | 8 ++ .../examples/recursiveStreams/rangeCall.ts | 5 + .../recursiveStreams/remoteRecCall.ts | 15 +++ .../recursiveStreams/yesNoStreamCall.ts | 16 +++ .../src/examples/recursiveStreamsCall.ts | 22 --- .../aqua/model/inline/RawValueInliner.scala | 4 +- .../scala/aqua/model/inline/TagInliner.scala | 16 ++- .../aqua/model/inline/ArrowInlinerSpec.scala | 2 +- .../aqua/model/inline/TagInlinerSpec.scala | 37 ++++- .../src/main/scala/aqua/raw/ops/RawTag.scala | 20 ++- .../main/scala/aqua/model/ValueModel.scala | 2 +- .../model/transform/pre/ArgsProvider.scala | 2 +- .../scala/aqua/parser/expr/func/ForExpr.scala | 15 ++- .../main/scala/aqua/parser/lexer/Token.scala | 1 + parser/src/test/scala/aqua/AquaSpec.scala | 3 + .../test/scala/aqua/parser/ForExprSpec.scala | 45 +++++-- .../main/scala/aqua/semantics/ExprSem.scala | 4 +- .../aqua/semantics/expr/func/ForSem.scala | 34 ++--- .../aqua/semantics/expr/func/ParSeqSem.scala | 4 +- .../scala/aqua/semantics/SemanticsSpec.scala | 26 +++- 30 files changed, 448 insertions(+), 120 deletions(-) delete mode 100644 integration-tests/aqua/examples/recursiveStreams.aqua create mode 100644 integration-tests/aqua/examples/recursiveStreams/multiRec.aqua create mode 100644 integration-tests/aqua/examples/recursiveStreams/nested.aqua create mode 100644 integration-tests/aqua/examples/recursiveStreams/pipeline.aqua create mode 100644 integration-tests/aqua/examples/recursiveStreams/range.aqua create mode 100644 integration-tests/aqua/examples/recursiveStreams/remoteRec.aqua create mode 100644 integration-tests/aqua/examples/recursiveStreams/yesNo.aqua create mode 100644 integration-tests/src/examples/recursiveStreams/multiRecStreamCall.ts create mode 100644 integration-tests/src/examples/recursiveStreams/nestedCall.ts create mode 100644 integration-tests/src/examples/recursiveStreams/pipelineCall.ts create mode 100644 integration-tests/src/examples/recursiveStreams/rangeCall.ts create mode 100644 integration-tests/src/examples/recursiveStreams/remoteRecCall.ts create mode 100644 integration-tests/src/examples/recursiveStreams/yesNoStreamCall.ts delete mode 100644 integration-tests/src/examples/recursiveStreamsCall.ts diff --git a/integration-tests/aqua/examples/recursiveStreams.aqua b/integration-tests/aqua/examples/recursiveStreams.aqua deleted file mode 100644 index fb8f8da5c..000000000 --- a/integration-tests/aqua/examples/recursiveStreams.aqua +++ /dev/null @@ -1,13 +0,0 @@ - -service YesNoService("yesno"): - get() -> string - -func recursiveStream() -> []string, []string: - result: *string - loop: *string - loop <<- "yes" - for l <- loop: - if l == "yes": - loop <- YesNoService.get() - result <<- "success" - <- result, loop diff --git a/integration-tests/aqua/examples/recursiveStreams/multiRec.aqua b/integration-tests/aqua/examples/recursiveStreams/multiRec.aqua new file mode 100644 index 000000000..6b6f50054 --- /dev/null +++ b/integration-tests/aqua/examples/recursiveStreams/multiRec.aqua @@ -0,0 +1,22 @@ +aqua MultiRec + +export TestService, multiRecStream + +service TestService("test-srv"): + handle(i: i32) -> []i32 + +func multiRecStream(init: i32, target: i32) -> []i32: + result: *string + loop: *i32 + + loop <<- init + for l <- loop rec: + news <- TestService.handle(l) + for n <- news: + loop <<- n + if l == target: + result <<- "done" + + join result! + + <- loop diff --git a/integration-tests/aqua/examples/recursiveStreams/nested.aqua b/integration-tests/aqua/examples/recursiveStreams/nested.aqua new file mode 100644 index 000000000..a508e0bb2 --- /dev/null +++ b/integration-tests/aqua/examples/recursiveStreams/nested.aqua @@ -0,0 +1,19 @@ +aqua Nested + +export nested + +func nested(n: u32) -> []u32: + result: *u32 + iterator: *u32 + + iterator <<- 0 + for i <- iterator rec: + if i < n: + for j <- iterator rec: + result <<- j + iterator <<- i + 1 + + if n > 0: + join result[n * (n + 1) / 2 - 1] + + <- result \ No newline at end of file diff --git a/integration-tests/aqua/examples/recursiveStreams/pipeline.aqua b/integration-tests/aqua/examples/recursiveStreams/pipeline.aqua new file mode 100644 index 000000000..787e6e23c --- /dev/null +++ b/integration-tests/aqua/examples/recursiveStreams/pipeline.aqua @@ -0,0 +1,29 @@ +aqua Pipeline + +export pipelineStream + +func pipelineStream(init: i32, target: i32) -> []i32: + result: *string + + loop1: *i32 + loop2: *i32 + loop3: *i32 + + loop1 <<- init + for l <- loop1 rec: + if l < target: + loop1 <<- l + 1 + loop2 <<- l * 3 + + for l <- loop2 rec: + loop3 <<- l + loop3 <<- l + 1 + loop3 <<- l + 2 + + for l <- loop3 rec: + if l == target: + result <<- "success" + + join result! + + <- loop3 diff --git a/integration-tests/aqua/examples/recursiveStreams/range.aqua b/integration-tests/aqua/examples/recursiveStreams/range.aqua new file mode 100644 index 000000000..4cd414c59 --- /dev/null +++ b/integration-tests/aqua/examples/recursiveStreams/range.aqua @@ -0,0 +1,18 @@ +aqua Range + +export range + +func range(a: i32, b: i32) -> []i32: + result: *i32 + iterator: *i32 + + iterator <<- a + for i <- iterator rec: + if i < b: + result <<- i + iterator <<- i + 1 + + if b > a: + join result[b - a - 1] + + <- result \ No newline at end of file diff --git a/integration-tests/aqua/examples/recursiveStreams/remoteRec.aqua b/integration-tests/aqua/examples/recursiveStreams/remoteRec.aqua new file mode 100644 index 000000000..ace2ed340 --- /dev/null +++ b/integration-tests/aqua/examples/recursiveStreams/remoteRec.aqua @@ -0,0 +1,19 @@ +aqua RemoteRec + +export RemoteSrv, remoteRecStream + +service RemoteSrv("remote-srv"): + handle(i: i32) -> i32 + +func remoteRecStream(init: i32, target: i32, friend: string, friendRelay: string) -> []i32: + loop: *i32 + + loop <<- init + for l <- loop rec: + on friend via friendRelay: + if l < target: + loop <- RemoteSrv.handle(l) + + join loop[target - init] + + <- loop diff --git a/integration-tests/aqua/examples/recursiveStreams/yesNo.aqua b/integration-tests/aqua/examples/recursiveStreams/yesNo.aqua new file mode 100644 index 000000000..d92700448 --- /dev/null +++ b/integration-tests/aqua/examples/recursiveStreams/yesNo.aqua @@ -0,0 +1,21 @@ +aqua YesNo + +export YesNoService, yesNoStream + +service YesNoService("yesno"): + get() -> string + +func yesNoStream() -> []string: + result: *string + loop: *string + + loop <<- "yes" + for l <- loop rec: + if l == "yes": + loop <- YesNoService.get() + else: + result <<- "success" + + join result! + + <- loop diff --git a/integration-tests/src/__test__/examples.spec.ts b/integration-tests/src/__test__/examples.spec.ts index 4c141342f..34b5a63db 100644 --- a/integration-tests/src/__test__/examples.spec.ts +++ b/integration-tests/src/__test__/examples.spec.ts @@ -90,7 +90,7 @@ import { streamArgsCall, modifyStreamCall, returnDerivedStreamCall, - lng280BugWithForEmptyStreamFuncCall + lng280BugWithForEmptyStreamFuncCall, } from "../examples/streamArgsCall.js"; import { streamResultsCall } from "../examples/streamResultsCall.js"; import { structuralTypingCall } from "../examples/structuralTypingCall.js"; @@ -135,7 +135,6 @@ import { joinIdxLocalCall, joinIdxRelayCall, } from "../examples/joinCall.js"; -import { recursiveStreamsCall } from "../examples/recursiveStreamsCall.js"; import { renameVarsCall } from "../examples/renameVars.js"; import { arraySugarCall, @@ -161,6 +160,12 @@ import { returnArrowCall, returnArrowChainCall, } from "../examples/returnArrowCall.js"; +import { rangeCall } from "../examples/recursiveStreams/rangeCall.js"; +import { nestedCall } from "../examples/recursiveStreams/nestedCall.js"; +import { yesNoStreamCall } from "../examples/recursiveStreams/yesNoStreamCall.js"; +import { multiRecStreamCall } from "../examples/recursiveStreams/multiRecStreamCall.js"; +import { pipelineStreamCall } from "../examples/recursiveStreams/pipelineCall.js"; +import { remoteRecStreamCall } from "../examples/recursiveStreams/remoteRecCall.js"; var selfPeerId: string; var peer1: IFluenceClient; @@ -217,6 +222,77 @@ describe("Testing examples", () => { await stop(); }); + describe("for ... rec", () => { + const range = (start: number, end: number) => + Array.from({ length: end - start }, (v, k) => k + start); + + it("range", async () => { + for (const i of range(-5, 5)) { + for (const j of range(-5, 5)) { + const result = await rangeCall(i, j); + if (i < j) { + expect(result).toEqual(range(i, j)); + } else { + expect(result).toEqual([]); + } + } + } + }, 15000); + + /** + * This test does not work due to Aqua VM + */ + it.skip("nested", async () => { + for (const i of range(0, 10)) { + const result = await nestedCall(i); + expect(result).toEqual(range(0, i).flatMap((x) => range(0, x + 1))); + } + }, 15000); + + it("yes|no stream", async () => { + for (const i of range(1, 10)) { + const yesNo = await yesNoStreamCall(i); + expect(yesNo).toEqual( + range(0, i) + .map((_) => "yes") + .concat(["no"]), + ); + } + }, 15000); + + it("multi rec stream", async () => { + const handle = (i: number) => { + if (i % 3 === 0) return [i + 1]; + if (i % 3 === 1) return [i + 1, i + 2]; + return []; + }; + for (const i of range(1, 10)) { + const loop = await multiRecStreamCall(0, i, handle); + range(0, i + 1).forEach((j) => { + expect(loop).toContain(j); + }); + } + }, 15000); + + it("pipeline", async () => { + for (const i of range(1, 10)) { + const result = await pipelineStreamCall(0, i); + expect(result.sort()).toEqual(range(0, i + 1)); + } + }, 15000); + + /** + * This test does not work due to `for ... rec` + * not taking topology into account + */ + it.skip("remote rec", async () => { + for (const i of range(0, 10)) { + const result = await remoteRecStreamCall(0, i, peer2); + expect(result).toEqual(range(0, i + 1)); + } + }, 15000); + }); + it("callArrow.aqua args bug 426", async () => { let argResult = await reproArgsBug426Call(); @@ -630,29 +706,41 @@ describe("Testing examples", () => { it.skip("streamArgs.aqua LNG-280 with for", async () => { let result = await lng280BugWithForCall(); expect(result).toEqual([ - "valueUseStream", - "valueReturnStream", - "valueUseStream", - "valueReturnStream", - "valueUseStream", - "valueReturnStream" + "valueUseStream", + "valueReturnStream", + "valueUseStream", + "valueReturnStream", + "valueUseStream", + "valueReturnStream", ]); }); it("streamArgs.aqua LNG-280 with for and anonymous stream", async () => { let result = await lng280BugWithForAnonStreamCall(); - expect(result).toEqual([[1, 1], [1, 2], [1, 3], [1, 4], [1, 5]]); + expect(result).toEqual([ + [1, 1], + [1, 2], + [1, 3], + [1, 4], + [1, 5], + ]); }); it("streamArgs.aqua LNG-280 with for and anonymous stream from function", async () => { - let result = await lng280BugWithForEmptyStreamFuncCall(); - expect(result).toEqual([[1, 1], [1, 2], [1, 3], [1, 4], [1, 5]]); - }); + let result = await lng280BugWithForEmptyStreamFuncCall(); + expect(result).toEqual([ + [1, 1], + [1, 2], + [1, 3], + [1, 4], + [1, 5], + ]); + }); it.skip("streamArgs.aqua return derived stream", async () => { - let result = await returnDerivedStreamCall(); - expect(result).toEqual([1]); - }); + let result = await returnDerivedStreamCall(); + expect(result).toEqual([1]); + }); it("streamResults.aqua", async () => { let streamResultsResult = await streamResultsCall(); @@ -822,15 +910,6 @@ describe("Testing examples", () => { // expect(res).toEqual("ok") // }); - // TODO: uncomment - // it('recursiveStreams.aqua', async () => { - // let [sucList, loopList] = await recursiveStreamsCall(); - // console.log(sucList); - // console.log(loopList); - // expect(loopList).toEqual(['yes', 'yes', 'yes', 'yes', 'no']); - // expect(sucList.length).toEqual(5); - // }); - it("renameVars.aqua", async () => { let renameVarsResult = await renameVarsCall(); expect(renameVarsResult).toEqual(["ok", "ok"]); diff --git a/integration-tests/src/examples/recursiveStreams/multiRecStreamCall.ts b/integration-tests/src/examples/recursiveStreams/multiRecStreamCall.ts new file mode 100644 index 000000000..9d27a6709 --- /dev/null +++ b/integration-tests/src/examples/recursiveStreams/multiRecStreamCall.ts @@ -0,0 +1,14 @@ +import { + multiRecStream, + registerTestService, +} from "../../compiled/examples/recursiveStreams/multiRec.js"; + +export async function multiRecStreamCall( + init: number, + target: number, + handle: (i: number) => number[], +): Promise { + registerTestService({ handle }); + + return await multiRecStream(init, target); +} diff --git a/integration-tests/src/examples/recursiveStreams/nestedCall.ts b/integration-tests/src/examples/recursiveStreams/nestedCall.ts new file mode 100644 index 000000000..0c3325eec --- /dev/null +++ b/integration-tests/src/examples/recursiveStreams/nestedCall.ts @@ -0,0 +1,5 @@ +import { nested } from "../../compiled/examples/recursiveStreams/nested.js"; + +export async function nestedCall(n: number): Promise { + return await nested(n); +} diff --git a/integration-tests/src/examples/recursiveStreams/pipelineCall.ts b/integration-tests/src/examples/recursiveStreams/pipelineCall.ts new file mode 100644 index 000000000..fbe9f6d00 --- /dev/null +++ b/integration-tests/src/examples/recursiveStreams/pipelineCall.ts @@ -0,0 +1,8 @@ +import { pipelineStream } from "../../compiled/examples/recursiveStreams/pipeline.js"; + +export async function pipelineStreamCall( + init: number, + target: number, +): Promise { + return await pipelineStream(init, target); +} diff --git a/integration-tests/src/examples/recursiveStreams/rangeCall.ts b/integration-tests/src/examples/recursiveStreams/rangeCall.ts new file mode 100644 index 000000000..45d5221ab --- /dev/null +++ b/integration-tests/src/examples/recursiveStreams/rangeCall.ts @@ -0,0 +1,5 @@ +import { range } from "../../compiled/examples/recursiveStreams/range.js"; + +export async function rangeCall(a: number, b: number): Promise { + return await range(a, b); +} diff --git a/integration-tests/src/examples/recursiveStreams/remoteRecCall.ts b/integration-tests/src/examples/recursiveStreams/remoteRecCall.ts new file mode 100644 index 000000000..81f300065 --- /dev/null +++ b/integration-tests/src/examples/recursiveStreams/remoteRecCall.ts @@ -0,0 +1,15 @@ +import { IFluenceClient } from "@fluencelabs/js-client"; +import { remoteRecStream } from "../../compiled/examples/recursiveStreams/remoteRec.js"; + +export async function remoteRecStreamCall( + init: number, + target: number, + peer: IFluenceClient, +): Promise { + return await remoteRecStream( + init, + target, + peer.getPeerId(), + peer.getRelayPeerId(), + ); +} diff --git a/integration-tests/src/examples/recursiveStreams/yesNoStreamCall.ts b/integration-tests/src/examples/recursiveStreams/yesNoStreamCall.ts new file mode 100644 index 000000000..8eef91f50 --- /dev/null +++ b/integration-tests/src/examples/recursiveStreams/yesNoStreamCall.ts @@ -0,0 +1,16 @@ +import { + yesNoStream, + registerYesNoService, +} from "../../compiled/examples/recursiveStreams/yesNo.js"; + +export async function yesNoStreamCall(limit: number): Promise { + let i = 1; + registerYesNoService({ + get: () => { + i += 1; + return i > limit ? "no" : "yes"; + }, + }); + + return await yesNoStream(); +} diff --git a/integration-tests/src/examples/recursiveStreamsCall.ts b/integration-tests/src/examples/recursiveStreamsCall.ts deleted file mode 100644 index de3bd1a54..000000000 --- a/integration-tests/src/examples/recursiveStreamsCall.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { - recursiveStream, - registerYesNoService, -} from "../compiled/examples/recursiveStreams.js"; - -export async function recursiveStreamsCall(): Promise<[string[], string[]]> { - let i = 0; - registerYesNoService({ - get: () => { - i++; - if (i > 3) { - console.log("return no"); - return "no"; - } else { - console.log("return yes"); - return "yes"; - } - }, - }); - - return await recursiveStream(); -} diff --git a/model/inline/src/main/scala/aqua/model/inline/RawValueInliner.scala b/model/inline/src/main/scala/aqua/model/inline/RawValueInliner.scala index 604733fd3..365c5bfa0 100644 --- a/model/inline/src/main/scala/aqua/model/inline/RawValueInliner.scala +++ b/model/inline/src/main/scala/aqua/model/inline/RawValueInliner.scala @@ -115,8 +115,8 @@ object RawValueInliner extends Logging { ): State[S, (CallModel, Option[OpModel.Tree])] = { valueListToModel(call.args).flatMap { args => if (flatStreamArguments) - args.map{ arg => - TagInliner.flat(arg._1, arg._2, true) + args.map { arg => + TagInliner.flat(arg._1, arg._2) }.sequence else State.pure(args) diff --git a/model/inline/src/main/scala/aqua/model/inline/TagInliner.scala b/model/inline/src/main/scala/aqua/model/inline/TagInliner.scala index f161348ec..e3146b172 100644 --- a/model/inline/src/main/scala/aqua/model/inline/TagInliner.scala +++ b/model/inline/src/main/scala/aqua/model/inline/TagInliner.scala @@ -149,11 +149,10 @@ object TagInliner extends Logging { def flat[S: Mangler]( vm: ValueModel, - op: Option[OpModel.Tree], - flatStream: Boolean + op: Option[OpModel.Tree] ): State[S, (ValueModel, Option[OpModel.Tree])] = { vm match { - case v @ VarModel(n, StreamType(t), l) if flatStream => + case ValueModel.Stream(v @ VarModel(n, _, l), StreamType(t)) => val canonName = n + "_canon" for { canonN <- Mangler[S].findAndForbidName(canonName) @@ -203,7 +202,7 @@ object TagInliner extends Logging { peerIdDe <- valueToModel(peerId) viaDe <- valueListToModel(via.toList) viaDeFlattened <- viaDe.traverse { case (vm, tree) => - flat(vm, tree, true) + flat(vm, tree) } (pid, pif) = peerIdDe (viaD, viaF) = viaDeFlattened.unzip @@ -238,7 +237,10 @@ object TagInliner extends Logging { case ForTag(item, iterable, mode) => for { vp <- valueToModel(iterable) - flattened <- flat(vp._1, vp._2, true) + flattened <- mode match { + case ForTag.Mode.RecMode => State.pure(vp) + case _ => flat(vp._1, vp._2) + } (v, p) = flattened n <- Mangler[S].findAndForbidName(item) elementType = iterable.`type` match { @@ -250,8 +252,8 @@ object TagInliner extends Logging { } _ <- Exports[S].resolved(item, VarModel(n, elementType)) modeModel = mode match { - case ForTag.Mode.Blocking => ForModel.Mode.Never - case ForTag.Mode.NonBlocking => ForModel.Mode.Null + case ForTag.Mode.SeqMode | ForTag.Mode.TryMode => ForModel.Mode.Null + case ForTag.Mode.ParMode | ForTag.Mode.RecMode => ForModel.Mode.Never } } yield TagInlined.Single( model = ForModel(n, v, modeModel), diff --git a/model/inline/src/test/scala/aqua/model/inline/ArrowInlinerSpec.scala b/model/inline/src/test/scala/aqua/model/inline/ArrowInlinerSpec.scala index 3d5d1fc07..3914c29ad 100644 --- a/model/inline/src/test/scala/aqua/model/inline/ArrowInlinerSpec.scala +++ b/model/inline/src/test/scala/aqua/model/inline/ArrowInlinerSpec.scala @@ -2245,7 +2245,7 @@ class ArrowInlinerSpec extends AnyFlatSpec with Matchers with Inside { ) val foldOp = ForTag - .blocking(iVar.name, array) + .par(iVar.name, array) .wrap( inFold, NextTag(iVar.name).leaf diff --git a/model/inline/src/test/scala/aqua/model/inline/TagInlinerSpec.scala b/model/inline/src/test/scala/aqua/model/inline/TagInlinerSpec.scala index 03d373cdd..447b67ae1 100644 --- a/model/inline/src/test/scala/aqua/model/inline/TagInlinerSpec.scala +++ b/model/inline/src/test/scala/aqua/model/inline/TagInlinerSpec.scala @@ -1,15 +1,20 @@ package aqua.model.inline -import aqua.model.{LiteralModel, OpModel, SeqModel} +import aqua.model.ForModel +import aqua.model.ValueModel +import aqua.model.inline.TagInliner.TagInlined import aqua.model.inline.state.InliningState +import aqua.model.{LiteralModel, OpModel, SeqModel} +import aqua.raw.ops.ForTag import aqua.raw.ops.{Call, CanonicalizeTag, FlattenTag} import aqua.raw.value.ValueRaw +import aqua.raw.value.VarRaw import aqua.types.{ScalarType, StreamType} -import org.scalatest.flatspec.AnyFlatSpec -import org.scalatest.matchers.should.Matchers + import cats.syntax.show.* import org.scalatest.Inside -import aqua.model.inline.TagInliner.TagInlined +import org.scalatest.flatspec.AnyFlatSpec +import org.scalatest.matchers.should.Matchers class TagInlinerSpec extends AnyFlatSpec with Matchers with Inside { @@ -47,9 +52,31 @@ class TagInlinerSpec extends AnyFlatSpec with Matchers with Inside { ValueRaw.Nil.value, ValueRaw.Nil.baseType ) - + inside(inlined) { case TagInlined.Empty(prefix) => prefix shouldBe None } } + + "ForTag" should "not canonicalize iterable in RecMode" in { + val iterableRaw = VarRaw("iterable", StreamType(ScalarType.string)) + val iterableModel = ValueModel.fromRaw(iterableRaw) + + val tag = ForTag("i", iterableRaw, ForTag.Mode.RecMode) + + val (state, inlined) = TagInliner + .tagToModel[InliningState](tag) + .run( + InliningState( + resolvedExports = Map( + iterableRaw.name -> iterableModel + ) + ) + ) + .value + + inside(inlined) { case TagInlined.Single(ForModel(_, iter, ForModel.Mode.Never), _) => + iter shouldBe iterableModel + } + } } diff --git a/model/raw/src/main/scala/aqua/raw/ops/RawTag.scala b/model/raw/src/main/scala/aqua/raw/ops/RawTag.scala index 347a74987..22666e0c6 100644 --- a/model/raw/src/main/scala/aqua/raw/ops/RawTag.scala +++ b/model/raw/src/main/scala/aqua/raw/ops/RawTag.scala @@ -183,16 +183,24 @@ case class ForTag(item: String, iterable: ValueRaw, mode: ForTag.Mode) extends S object ForTag { + /** + * | Syntax | mode | fold last | canon | inner tag | par null wrap | + * |-------------|:----:|:---------:|:-----:|:---------:|:-------------:| + * | for ... | seq | null | + | seq | - | + * | for ... par | par | never | + | par | + | + * | for ... try | try | null | + | try | - | + * | for ... rec | rec | never | - | par | + | + * | parseq ... | par | never | + | par | - | + */ enum Mode { - case Blocking - case NonBlocking + case ParMode, SeqMode, TryMode, RecMode } - def blocking(item: String, iterable: ValueRaw): ForTag = - ForTag(item, iterable, Mode.Blocking) + def par(item: String, iterable: ValueRaw): ForTag = + ForTag(item, iterable, Mode.ParMode) - def nonBlocking(item: String, iterable: ValueRaw): ForTag = - ForTag(item, iterable, Mode.NonBlocking) + def seq(item: String, iterable: ValueRaw): ForTag = + ForTag(item, iterable, Mode.SeqMode) } case class CallArrowRawTag( diff --git a/model/src/main/scala/aqua/model/ValueModel.scala b/model/src/main/scala/aqua/model/ValueModel.scala index 6a795f045..2a2997f52 100644 --- a/model/src/main/scala/aqua/model/ValueModel.scala +++ b/model/src/main/scala/aqua/model/ValueModel.scala @@ -78,7 +78,7 @@ object ValueModel { def unapply(vm: VarModel): Option[(VarModel, StreamType)] = vm match { - case vm@VarModel(_, t: StreamType, _) => + case vm @ VarModel(_, t: StreamType, _) => (vm, t).some case _ => none } diff --git a/model/transform/src/main/scala/aqua/model/transform/pre/ArgsProvider.scala b/model/transform/src/main/scala/aqua/model/transform/pre/ArgsProvider.scala index 909a526e8..a971dece2 100644 --- a/model/transform/src/main/scala/aqua/model/transform/pre/ArgsProvider.scala +++ b/model/transform/src/main/scala/aqua/model/transform/pre/ArgsProvider.scala @@ -36,7 +36,7 @@ case class ArgsFromService(dataServiceId: ValueRaw) extends ArgsProvider { ) .leaf, ForTag - .nonBlocking(item, VarRaw(iter, ArrayType(t.element))) + .seq(item, VarRaw(iter, ArrayType(t.element))) .wrap( SeqTag.wrap( PushToStreamTag(VarRaw(item, t.element), Call.Export(varName, t)).leaf, diff --git a/parser/src/main/scala/aqua/parser/expr/func/ForExpr.scala b/parser/src/main/scala/aqua/parser/expr/func/ForExpr.scala index 13d7a90ec..0122deb80 100644 --- a/parser/src/main/scala/aqua/parser/expr/func/ForExpr.scala +++ b/parser/src/main/scala/aqua/parser/expr/func/ForExpr.scala @@ -6,12 +6,13 @@ import aqua.parser.lexer.Token.* import aqua.parser.lexer.{Name, ValueToken} import aqua.parser.lift.LiftParser import aqua.parser.lift.LiftParser.* -import cats.parse.Parser as P -import cats.syntax.comonad.* -import cats.{~>, Comonad} import aqua.parser.lift.Span import aqua.parser.lift.Span.{P0ToSpan, PToSpan} +import cats.parse.Parser as P +import cats.syntax.comonad.* +import cats.{Comonad, ~>} + case class ForExpr[F[_]]( item: Name[F], iterable: ValueToken[F], @@ -23,12 +24,16 @@ case class ForExpr[F[_]]( } object ForExpr extends Expr.AndIndented { - enum Mode { case ParMode, TryMode } + enum Mode { case ParMode, TryMode, RecMode } override def validChildren: List[Expr.Lexem] = ArrowExpr.funcChildren private lazy val modeP: P[Mode] = - (` ` *> (`par`.as(Mode.ParMode) | `try`.as(Mode.TryMode)).lift).map(_.extract) + (` ` *> ( + `par`.as(Mode.ParMode) | + `try`.as(Mode.TryMode) | + `rec`.as(Mode.RecMode) + ).lift).map(_.extract) override def p: P[ForExpr[Span.S]] = ((`for` *> ` ` *> Name.p <* ` <- `) ~ ValueToken.`value` ~ modeP.?).map { diff --git a/parser/src/main/scala/aqua/parser/lexer/Token.scala b/parser/src/main/scala/aqua/parser/lexer/Token.scala index 318e35bfe..7f65dc9bd 100644 --- a/parser/src/main/scala/aqua/parser/lexer/Token.scala +++ b/parser/src/main/scala/aqua/parser/lexer/Token.scala @@ -56,6 +56,7 @@ object Token { val `via`: P[Unit] = P.string("via") val `%init_peer_id%` : P[Unit] = P.string("%init_peer_id%") val `for`: P[Unit] = P.string("for") + val `rec`: P[Unit] = P.string("rec") val `if`: P[Unit] = P.string("if") val `eqs`: P[Unit] = P.string("==") val `neq`: P[Unit] = P.string("!=") diff --git a/parser/src/test/scala/aqua/AquaSpec.scala b/parser/src/test/scala/aqua/AquaSpec.scala index 6aca46db4..07080fe85 100644 --- a/parser/src/test/scala/aqua/AquaSpec.scala +++ b/parser/src/test/scala/aqua/AquaSpec.scala @@ -67,6 +67,9 @@ object AquaSpec { def toBool(n: Boolean): LiteralToken[Id] = LiteralToken[Id](n.toString, bool) def toStr(n: String): LiteralToken[Id] = LiteralToken[Id]("\"" + n + "\"", string) + def toArr(arr: List[ValueToken[Id]]): CollectionToken[Id] = + CollectionToken[Id](CollectionToken.Mode.ArrayMode, arr) + def toNamedType(str: String): NamedTypeToken[Id] = NamedTypeToken[Id](str) def toArrayType(str: String): ArrayTypeToken[Id] = ArrayTypeToken[Id]((), str) diff --git a/parser/src/test/scala/aqua/parser/ForExprSpec.scala b/parser/src/test/scala/aqua/parser/ForExprSpec.scala index ec0ab2779..e89f9571c 100644 --- a/parser/src/test/scala/aqua/parser/ForExprSpec.scala +++ b/parser/src/test/scala/aqua/parser/ForExprSpec.scala @@ -2,6 +2,7 @@ package aqua.parser import aqua.AquaSpec import aqua.parser.expr.func.ForExpr + import cats.Id import org.scalatest.flatspec.AnyFlatSpec import org.scalatest.matchers.should.Matchers @@ -9,30 +10,48 @@ import org.scalatest.matchers.should.Matchers class ForExprSpec extends AnyFlatSpec with Matchers with AquaSpec { import AquaSpec.{given, *} - "for expression" should "be parsed" in { - parseFor("for some <- \"a\"") should be( - ForExpr[Id]("some", toStr("a"), None) + def forTestSuite( + modeStr: String, + mode: Option[ForExpr.Mode] + ): Unit = { + parseFor(s"for some <- 1$modeStr") should be( + ForExpr[Id]("some", toNumber(1), mode) ) - parseFor("for some <- \"a\"") should be( - ForExpr[Id]("some", toStr("a"), None) + parseFor(s"for some <- false$modeStr") should be( + ForExpr[Id]("some", toBool(false), mode) ) - parseFor("for some <- 1") should be( - ForExpr[Id]("some", toNumber(1), None) + parseFor(s"for some <- \"a\"$modeStr") should be( + ForExpr[Id]("some", toStr("a"), mode) ) - parseFor("for some <- false") should be( - ForExpr[Id]("some", toBool(false), None) + parseFor(s"for i <- []$modeStr") should be( + ForExpr[Id]("i", toArr(Nil), mode) ) - parseFor("for some <- false par") should be( - ForExpr[Id]("some", toBool(false), Some(ForExpr.Mode.ParMode)) + parseFor(s"for i <- [1, 2, 3]$modeStr") should be( + ForExpr[Id]("i", toArr(List(toNumber(1), toNumber(2), toNumber(3))), mode) ) - parseFor("for some <- false try") should be( - ForExpr[Id]("some", toBool(false), Some(ForExpr.Mode.TryMode)) + parseFor(s"for i <- stream$modeStr") should be( + ForExpr[Id]("i", toVar("stream"), mode) ) + } + + "for expression" should "be parsed" in { + forTestSuite("", None) + } + + "for par expression" should "be parsed" in { + forTestSuite(" par", Some(ForExpr.Mode.ParMode)) + } + + "for try expression" should "be parsed" in { + forTestSuite(" try", Some(ForExpr.Mode.TryMode)) + } + "for rec expression" should "be parsed" in { + forTestSuite(" rec", Some(ForExpr.Mode.RecMode)) } } diff --git a/semantics/src/main/scala/aqua/semantics/ExprSem.scala b/semantics/src/main/scala/aqua/semantics/ExprSem.scala index 9cb4fb7c7..1c04cd957 100644 --- a/semantics/src/main/scala/aqua/semantics/ExprSem.scala +++ b/semantics/src/main/scala/aqua/semantics/ExprSem.scala @@ -1,9 +1,9 @@ package aqua.semantics -import aqua.raw.Raw import aqua.parser.Expr import aqua.parser.expr.* import aqua.parser.expr.func.* +import aqua.raw.Raw import aqua.semantics.expr.* import aqua.semantics.expr.func.* import aqua.semantics.rules.ValuesAlgebra @@ -20,7 +20,7 @@ object ExprSem { def getProg[S[_], G[_]: Monad]( expr: Expr[S] - )(implicit + )(using A: AbilitiesAlgebra[S, G], N: NamesAlgebra[S, G], T: TypesAlgebra[S, G], diff --git a/semantics/src/main/scala/aqua/semantics/expr/func/ForSem.scala b/semantics/src/main/scala/aqua/semantics/expr/func/ForSem.scala index 1e384bf63..0a2f8c745 100644 --- a/semantics/src/main/scala/aqua/semantics/expr/func/ForSem.scala +++ b/semantics/src/main/scala/aqua/semantics/expr/func/ForSem.scala @@ -1,11 +1,10 @@ package aqua.semantics.expr.func import aqua.parser.expr.func.ForExpr -import aqua.parser.expr.func.ForExpr.Mode import aqua.parser.lexer.{Name, ValueToken} import aqua.raw.Raw import aqua.raw.ops.* -import aqua.raw.ops.ForTag +import aqua.raw.ops.ForTag.Mode import aqua.raw.value.ValueRaw import aqua.semantics.Prog import aqua.semantics.expr.func.FuncOpSem @@ -40,18 +39,17 @@ class ForSem[S[_]](val expr: ForExpr[S]) extends AnyVal { (iterable, ops) match { case (Some(vm), FuncOp(op)) => FuncOpSem.restrictStreamsInScope(op).map { restricted => - val innerTag = expr.mode.fold(SeqTag) { - case ForExpr.Mode.ParMode => ParTag - case ForExpr.Mode.TryMode => TryTag + val mode = expr.mode.fold(ForTag.Mode.SeqMode) { + case ForExpr.Mode.ParMode => ForTag.Mode.ParMode + case ForExpr.Mode.TryMode => ForTag.Mode.TryMode + case ForExpr.Mode.RecMode => ForTag.Mode.RecMode } - /** - * `for ... par` => blocking (`never` as `last` in `fold`) - * `for` and `for ... try` => non blocking (`null` as `last` in `fold`) - */ - val mode = expr.mode.fold(ForTag.Mode.NonBlocking) { - case ForExpr.Mode.ParMode => ForTag.Mode.Blocking - case Mode.TryMode => ForTag.Mode.NonBlocking + val innerTag = mode match { + case ForTag.Mode.SeqMode => SeqTag + case ForTag.Mode.ParMode => ParTag + case ForTag.Mode.TryMode => TryTag + case ForTag.Mode.RecMode => ParTag } val forTag = ForTag(expr.item.value, vm, mode).wrap( @@ -61,9 +59,15 @@ class ForSem[S[_]](val expr: ForExpr[S]) extends AnyVal { ) ) - // Fix: continue execution after fold par immediately, without finding a path out from par branches - if (innerTag == ParTag) ParTag.Detach.wrap(forTag).toFuncOp - else forTag.toFuncOp + // Fix: continue execution after fold par immediately, + // without finding a path out from par branches + val result = mode match { + case ForTag.Mode.ParMode | ForTag.Mode.RecMode => + ParTag.Detach.wrap(forTag) + case _ => forTag + } + + result.toFuncOp } case _ => Raw.error("Wrong body of the `for` expression").pure[F] } diff --git a/semantics/src/main/scala/aqua/semantics/expr/func/ParSeqSem.scala b/semantics/src/main/scala/aqua/semantics/expr/func/ParSeqSem.scala index 670154e75..852f95021 100644 --- a/semantics/src/main/scala/aqua/semantics/expr/func/ParSeqSem.scala +++ b/semantics/src/main/scala/aqua/semantics/expr/func/ParSeqSem.scala @@ -64,11 +64,11 @@ class ParSeqSem[S[_]](val expr: ParSeqExpr[S]) extends AnyVal { strategy = OnTag.ReturnStrategy.Relay.some ) /** - * `parseq` => blocking (`never` as `last` in `fold`) + * `parseq` => par (`never` as `last` in `fold`) * So that peer initiating `parseq` would not continue execution past it */ tag = ForTag - .blocking(expr.item.value, vm) + .par(expr.item.value, vm) .wrap( ParTag.wrap( onTag.wrap(restricted), diff --git a/semantics/src/test/scala/aqua/semantics/SemanticsSpec.scala b/semantics/src/test/scala/aqua/semantics/SemanticsSpec.scala index 2da479bb9..77837f528 100644 --- a/semantics/src/test/scala/aqua/semantics/SemanticsSpec.scala +++ b/semantics/src/test/scala/aqua/semantics/SemanticsSpec.scala @@ -607,7 +607,7 @@ class SemanticsSpec extends AnyFlatSpec with Matchers with Inside { |""".stripMargin insideBody(script) { body => - matchSubtree(body) { case (ForTag("p", _, ForTag.Mode.Blocking), forTag) => + matchSubtree(body) { case (ForTag("p", _, ForTag.Mode.ParMode), forTag) => matchChildren(forTag) { case (ParTag, parTag) => matchChildren(parTag)( { case (OnTag(_, _, strat), _) => @@ -620,6 +620,30 @@ class SemanticsSpec extends AnyFlatSpec with Matchers with Inside { } } + it should "generate right model for `for ... rec`" in { + val script = """ + |func test(): + | stream: *i32 + | for i <- stream rec: + | stream <<- i + |""".stripMargin + + insideBody(script) { body => + matchSubtree(body) { case (ForTag("i", stream, ForTag.Mode.RecMode), forTag) => + stream.`type` shouldBe StreamType(ScalarType.i32) + matchChildren(forTag) { case (ParTag, parTag) => + matchChildren(parTag)( + { case (PushToStreamTag(VarRaw(varName, _), Call.Export(streamName, _)), _) => + varName shouldBe "i" + streamName shouldBe "stream" + }, + { case (NextTag("i"), _) => } + ) + } + } + } + } + it should "forbid abilities or streams in struct fields" in { val scriptAbility = """