From ca6cae96ad27f07fe2e6c05ee1caa16153c0c991 Mon Sep 17 00:00:00 2001 From: InversionSpaces Date: Wed, 27 Sep 2023 11:52:52 +0200 Subject: [PATCH] feat(compiler): Make `if` propagate errors [fixes LNG-202] (#779) * Change if inlining, add fail model * Inline if * Fix, add comments * Add integration test * Fix test * Fix test * toBe -> toEqual --------- Co-authored-by: Dima --- .../aqua/examples/ifPropagateErrors.aqua | 39 ++++ .../src/__test__/examples.spec.ts | 6 + .../src/examples/ifPropagateErrors.ts | 15 ++ .../scala/aqua/model/inline/TagInliner.scala | 61 +----- .../aqua/model/inline/tag/IfTagInliner.scala | 197 ++++++++++++++++++ .../main/scala/aqua/model/ValueModel.scala | 9 +- .../aqua/semantics/expr/func/IfSem.scala | 2 +- 7 files changed, 269 insertions(+), 60 deletions(-) create mode 100644 integration-tests/aqua/examples/ifPropagateErrors.aqua create mode 100644 integration-tests/src/examples/ifPropagateErrors.ts create mode 100644 model/inline/src/main/scala/aqua/model/inline/tag/IfTagInliner.scala diff --git a/integration-tests/aqua/examples/ifPropagateErrors.aqua b/integration-tests/aqua/examples/ifPropagateErrors.aqua new file mode 100644 index 000000000..354a864f0 --- /dev/null +++ b/integration-tests/aqua/examples/ifPropagateErrors.aqua @@ -0,0 +1,39 @@ +aqua IfPropagateErrors + +export ifPropagateErrors, TestService + +service TestService("test-srv"): + call(s: string) -> string + +func ifPropagateErrors() -> []string: + stream: *string + + a <- TestService.call("a") + b <- TestService.call("b") + + try: + if a == b || a == "a": -- true + stream <- TestService.call("fail") + else: + stream <- TestService.call("else1") + otherwise: + stream <- TestService.call("otherwise1") + + try: + if a != b: -- true + stream <- TestService.call("fail") + otherwise: + stream <- TestService.call("otherwise2") + + try: + if b == "b": --true + if a == "a": -- true + stream <- TestService.call("fail") + else: + stream <- TestService.call("else3") + else: + stream <- TestService.call("else4") + otherwise: + stream <- TestService.call("otherwise3") + + <- stream \ No newline at end of file diff --git a/integration-tests/src/__test__/examples.spec.ts b/integration-tests/src/__test__/examples.spec.ts index 071b6fa72..d595e3494 100644 --- a/integration-tests/src/__test__/examples.spec.ts +++ b/integration-tests/src/__test__/examples.spec.ts @@ -21,6 +21,7 @@ import { registerPrintln } from "../compiled/examples/println.js"; import { helloWorldCall } from "../examples/helloWorldCall.js"; import { foldBug499Call, foldCall } from "../examples/foldCall.js"; import { bugNG69Call, ifCall, ifWrapCall } from "../examples/ifCall.js"; +import { ifPropagateErrorsCall } from "../examples/ifPropagateErrors.js"; import { parCall, testTimeoutCall } from "../examples/parCall.js"; import { complexCall } from "../examples/complex.js"; import { @@ -269,6 +270,11 @@ describe("Testing examples", () => { expect(res).toBe(true); }); + it("ifPropagateErrors.aqua", async () => { + let res = await ifPropagateErrorsCall(); + expect(res).toEqual([1, 2, 3].map((i) => "otherwise" + i)); + }); + it("helloWorld.aqua", async () => { let helloWorldResult = await helloWorldCall(); expect(helloWorldResult).toBe("Hello, NAME!"); diff --git a/integration-tests/src/examples/ifPropagateErrors.ts b/integration-tests/src/examples/ifPropagateErrors.ts new file mode 100644 index 000000000..6a3d79b02 --- /dev/null +++ b/integration-tests/src/examples/ifPropagateErrors.ts @@ -0,0 +1,15 @@ +import { + ifPropagateErrors, + registerTestService, +} from "../compiled/examples/ifPropagateErrors.js"; + +export async function ifPropagateErrorsCall() { + registerTestService({ + call: (s) => { + if (s == "fail") return Promise.reject(s); + else return Promise.resolve(s); + }, + }); + + return await ifPropagateErrors(); +} 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 eba9d5938..27d1ac4fa 100644 --- a/model/inline/src/main/scala/aqua/model/inline/TagInliner.scala +++ b/model/inline/src/main/scala/aqua/model/inline/TagInliner.scala @@ -10,6 +10,7 @@ import aqua.raw.ops.* import aqua.raw.value.* import aqua.types.{BoxType, CanonStreamType, DataType, StreamType} import aqua.model.inline.Inline.parDesugarPrefixOpt +import aqua.model.inline.tag.IfTagInliner import cats.syntax.traverse.* import cats.syntax.applicative.* @@ -209,64 +210,12 @@ object TagInliner extends Logging { ) case IfTag(valueRaw) => - (valueRaw match { - // Optimize in case last operation is equality check - case ApplyBinaryOpRaw(op @ (BinOp.Eq | BinOp.Neq), left, right) => - ( - valueToModel(left) >>= canonicalizeIfStream, - valueToModel(right) >>= canonicalizeIfStream - ).mapN { case ((lmodel, lprefix), (rmodel, rprefix)) => - val prefix = parDesugarPrefixOpt(lprefix, rprefix) - val matchModel = MatchMismatchModel( - left = lmodel, - right = rmodel, - shouldMatch = op match { - case BinOp.Eq => true - case BinOp.Neq => false - } - ) - - (prefix, matchModel) - } - case _ => - valueToModel(valueRaw).map { case (valueModel, prefix) => - val matchModel = MatchMismatchModel( - left = valueModel, - right = LiteralModel.bool(true), - shouldMatch = true - ) - - (prefix, matchModel) - } - }).map { case (prefix, matchModel) => - val toModel = (children: Chain[OpModel.Tree]) => - XorModel.wrap( - children.uncons.map { case (ifBody, elseBody) => - val elseBodyFiltered = elseBody.filterNot( - _.head == EmptyModel - ) - - /** - * Hack for xor with mismatch always have second branch - * TODO: Fix this in topology - * see https://linear.app/fluence/issue/LNG-69/if-inside-on-produces-invalid-topology - */ - val elseBodyAugmented = - if (elseBodyFiltered.isEmpty) - Chain.one( - NullModel.leaf - ) - else elseBodyFiltered - - matchModel.wrap(ifBody) +: elseBodyAugmented - }.getOrElse(children) - ) - + IfTagInliner(valueRaw).inlined.map(inlined => TagInlined.Mapping( - toModel = toModel, - prefix = prefix + toModel = inlined.toModel, + prefix = inlined.prefix ) - } + ) case TryTag => pure(XorModel) diff --git a/model/inline/src/main/scala/aqua/model/inline/tag/IfTagInliner.scala b/model/inline/src/main/scala/aqua/model/inline/tag/IfTagInliner.scala new file mode 100644 index 000000000..900a8283f --- /dev/null +++ b/model/inline/src/main/scala/aqua/model/inline/tag/IfTagInliner.scala @@ -0,0 +1,197 @@ +package aqua.model.inline.tag + +import aqua.raw.value.{ApplyBinaryOpRaw, ValueRaw} +import aqua.raw.value.ApplyBinaryOpRaw.Op as BinOp +import aqua.model.ValueModel +import aqua.model.* +import aqua.model.inline.state.{Arrows, Exports, Mangler} +import aqua.model.inline.RawValueInliner.valueToModel +import aqua.model.inline.TagInliner.canonicalizeIfStream +import aqua.model.inline.Inline.parDesugarPrefixOpt + +import cats.data.Chain +import cats.syntax.flatMap.* +import cats.syntax.apply.* + +final case class IfTagInliner( + valueRaw: ValueRaw +) { + import IfTagInliner.* + + def inlined[S: Mangler: Exports: Arrows] = + (valueRaw match { + // Optimize in case last operation is equality check + case ApplyBinaryOpRaw(op @ (BinOp.Eq | BinOp.Neq), left, right) => + ( + valueToModel(left) >>= canonicalizeIfStream, + valueToModel(right) >>= canonicalizeIfStream + ).mapN { case ((lmodel, lprefix), (rmodel, rprefix)) => + val prefix = parDesugarPrefixOpt(lprefix, rprefix) + val shouldMatch = op match { + case BinOp.Eq => true + case BinOp.Neq => false + } + + (prefix, lmodel, rmodel, shouldMatch) + } + case _ => + valueToModel(valueRaw).map { case (valueModel, prefix) => + val compareModel = LiteralModel.bool(true) + val shouldMatch = true + + (prefix, valueModel, compareModel, shouldMatch) + } + }).map { case (prefix, leftValue, rightValue, shouldMatch) => + IfTagInlined( + prefix, + toModel(leftValue, rightValue, shouldMatch) + ) + } + + private def toModel( + leftValue: ValueModel, + rightValue: ValueModel, + shouldMatch: Boolean + )(children: Chain[OpModel.Tree]): OpModel.Tree = + children + .filterNot(_.head == EmptyModel) + .uncons + .map { case (ifBody, elseBody) => + val matchFailedErrorCode = + if (shouldMatch) LiteralModel.matchValuesNotEqualErrorCode + else LiteralModel.mismatchValuesEqualErrorCode + + /** + * (xor + * ([mis]match left right + * + * ) + * (seq + * (ap :error: -if-error-) + * (xor + * (match :error:.$.error_code [MIS]MATCH_FAILED_ERROR_CODE + * + * ) + * + * ) + * ) + * ) + */ + def runIf( + falseCase: Chain[OpModel.Tree], + errorCase: OpModel.Tree + ): OpModel.Tree = + XorModel.wrap( + MatchMismatchModel( + leftValue, + rightValue, + shouldMatch + ).wrap(ifBody), + SeqModel.wrap( + saveError(ifErrorName).leaf, + XorModel.wrap( + MatchMismatchModel( + ValueModel.lastErrorCode, + matchFailedErrorCode, + shouldMatch = true + ).wrap(falseCase), + errorCase + ) + ) + ) + + if (elseBody.isEmpty) + restrictErrors( + ifErrorName + )( + runIf( + falseCase = Chain.one(NullModel.leaf), + errorCase = failWithError(ifErrorName).leaf + ) + ) + else + restrictErrors( + ifErrorName, + elseErrorName, + ifElseErrorName + )( + runIf( + falseCase = elseBody, + /** + * (seq + * (ap :error: -else-error-) + * (xor + * (mismatch :error:.$.error_code [MIS]MATCH_FAILED_ERROR_CODE + * (ap -else-error- -if-else-error-) + * ) + * (ap -if-error- -if-else-error) + * ) + * (fail -if-else-error) + * ) + */ + errorCase = SeqModel.wrap( + saveError(elseErrorName).leaf, + XorModel.wrap( + MatchMismatchModel( + ValueModel.lastErrorCode, + LiteralModel.matchValuesNotEqualErrorCode, + shouldMatch = true + ).wrap( + renameError( + ifErrorName, + ifElseErrorName + ).leaf + ), + renameError( + elseErrorName, + ifElseErrorName + ).leaf + ), + failWithError(ifElseErrorName).leaf + ) + ) + ) + } + .getOrElse(EmptyModel.leaf) + +} + +object IfTagInliner { + + final case class IfTagInlined( + prefix: Option[OpModel.Tree], + toModel: Chain[OpModel.Tree] => OpModel.Tree + ) + + private def restrictErrors( + name: String* + )(tree: OpModel.Tree): OpModel.Tree = + name.foldLeft(tree) { case (tree, name) => + RestrictionModel( + name, + ValueModel.errorType + ).wrap(tree) + } + + private def saveError(name: String): FlattenModel = + FlattenModel( + ValueModel.error, + name + ) + + private def renameError(from: String, to: String): FlattenModel = + FlattenModel( + VarModel(from, ValueModel.errorType), + to + ) + + private def failWithError(name: String): FailModel = + FailModel( + VarModel(name, ValueModel.errorType) + ) + + private val ifErrorName = "-if-error-" + private val elseErrorName = "-else-error-" + private val ifElseErrorName = "-if-else-error-" + +} diff --git a/model/src/main/scala/aqua/model/ValueModel.scala b/model/src/main/scala/aqua/model/ValueModel.scala index bf38aa223..794acf014 100644 --- a/model/src/main/scala/aqua/model/ValueModel.scala +++ b/model/src/main/scala/aqua/model/ValueModel.scala @@ -91,9 +91,12 @@ object LiteralModel { } } - // AquaVM will return empty string for - // %last_error%.$.error_code if there is no %last_error% - val emptyErrorCode = quote("") + // AquaVM will return 0 for + // :error:.$.error_code if there is no :error: + val emptyErrorCode = number(0) + + val matchValuesNotEqualErrorCode = number(10001) + val mismatchValuesEqualErrorCode = number(10002) def fromRaw(raw: LiteralRaw): LiteralModel = LiteralModel(raw.value, raw.baseType) diff --git a/semantics/src/main/scala/aqua/semantics/expr/func/IfSem.scala b/semantics/src/main/scala/aqua/semantics/expr/func/IfSem.scala index 448a9745a..43acdabe1 100644 --- a/semantics/src/main/scala/aqua/semantics/expr/func/IfSem.scala +++ b/semantics/src/main/scala/aqua/semantics/expr/func/IfSem.scala @@ -22,7 +22,7 @@ import aqua.types.ScalarType class IfSem[S[_]](val expr: IfExpr[S]) extends AnyVal { - def program[Alg[_]: Monad](implicit + def program[Alg[_]: Monad](using V: ValuesAlgebra[S, Alg], T: TypesAlgebra[S, Alg], A: AbilitiesAlgebra[S, Alg],