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 <dmitry.shakhtarin@fluence.ai>
This commit is contained in:
InversionSpaces 2023-09-27 11:52:52 +02:00 committed by GitHub
parent f158074c4e
commit ca6cae96ad
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 269 additions and 60 deletions

View File

@ -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

View File

@ -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!");

View File

@ -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();
}

View File

@ -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)

View File

@ -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
* <ifBody>
* )
* (seq
* (ap :error: -if-error-)
* (xor
* (match :error:.$.error_code [MIS]MATCH_FAILED_ERROR_CODE
* <falseCase>
* )
* <errorCase>
* )
* )
* )
*/
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-"
}

View File

@ -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)

View File

@ -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],