mirror of
https://github.com/fluencelabs/aqua.git
synced 2024-12-04 14:40:17 +00:00
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:
parent
f158074c4e
commit
ca6cae96ad
39
integration-tests/aqua/examples/ifPropagateErrors.aqua
Normal file
39
integration-tests/aqua/examples/ifPropagateErrors.aqua
Normal 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
|
@ -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!");
|
||||
|
15
integration-tests/src/examples/ifPropagateErrors.ts
Normal file
15
integration-tests/src/examples/ifPropagateErrors.ts
Normal 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();
|
||||
}
|
@ -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)
|
||||
|
||||
|
@ -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-"
|
||||
|
||||
}
|
@ -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)
|
||||
|
||||
|
@ -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],
|
||||
|
Loading…
Reference in New Issue
Block a user