mirror of
https://github.com/fluencelabs/aqua.git
synced 2025-03-15 11:40:50 +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 { helloWorldCall } from "../examples/helloWorldCall.js";
|
||||||
import { foldBug499Call, foldCall } from "../examples/foldCall.js";
|
import { foldBug499Call, foldCall } from "../examples/foldCall.js";
|
||||||
import { bugNG69Call, ifCall, ifWrapCall } from "../examples/ifCall.js";
|
import { bugNG69Call, ifCall, ifWrapCall } from "../examples/ifCall.js";
|
||||||
|
import { ifPropagateErrorsCall } from "../examples/ifPropagateErrors.js";
|
||||||
import { parCall, testTimeoutCall } from "../examples/parCall.js";
|
import { parCall, testTimeoutCall } from "../examples/parCall.js";
|
||||||
import { complexCall } from "../examples/complex.js";
|
import { complexCall } from "../examples/complex.js";
|
||||||
import {
|
import {
|
||||||
@ -269,6 +270,11 @@ describe("Testing examples", () => {
|
|||||||
expect(res).toBe(true);
|
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 () => {
|
it("helloWorld.aqua", async () => {
|
||||||
let helloWorldResult = await helloWorldCall();
|
let helloWorldResult = await helloWorldCall();
|
||||||
expect(helloWorldResult).toBe("Hello, NAME!");
|
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.raw.value.*
|
||||||
import aqua.types.{BoxType, CanonStreamType, DataType, StreamType}
|
import aqua.types.{BoxType, CanonStreamType, DataType, StreamType}
|
||||||
import aqua.model.inline.Inline.parDesugarPrefixOpt
|
import aqua.model.inline.Inline.parDesugarPrefixOpt
|
||||||
|
import aqua.model.inline.tag.IfTagInliner
|
||||||
|
|
||||||
import cats.syntax.traverse.*
|
import cats.syntax.traverse.*
|
||||||
import cats.syntax.applicative.*
|
import cats.syntax.applicative.*
|
||||||
@ -209,64 +210,12 @@ object TagInliner extends Logging {
|
|||||||
)
|
)
|
||||||
|
|
||||||
case IfTag(valueRaw) =>
|
case IfTag(valueRaw) =>
|
||||||
(valueRaw match {
|
IfTagInliner(valueRaw).inlined.map(inlined =>
|
||||||
// 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)
|
|
||||||
)
|
|
||||||
|
|
||||||
TagInlined.Mapping(
|
TagInlined.Mapping(
|
||||||
toModel = toModel,
|
toModel = inlined.toModel,
|
||||||
prefix = prefix
|
prefix = inlined.prefix
|
||||||
)
|
)
|
||||||
}
|
)
|
||||||
|
|
||||||
case TryTag => pure(XorModel)
|
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
|
// AquaVM will return 0 for
|
||||||
// %last_error%.$.error_code if there is no %last_error%
|
// :error:.$.error_code if there is no :error:
|
||||||
val emptyErrorCode = quote("")
|
val emptyErrorCode = number(0)
|
||||||
|
|
||||||
|
val matchValuesNotEqualErrorCode = number(10001)
|
||||||
|
val mismatchValuesEqualErrorCode = number(10002)
|
||||||
|
|
||||||
def fromRaw(raw: LiteralRaw): LiteralModel = LiteralModel(raw.value, raw.baseType)
|
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 {
|
class IfSem[S[_]](val expr: IfExpr[S]) extends AnyVal {
|
||||||
|
|
||||||
def program[Alg[_]: Monad](implicit
|
def program[Alg[_]: Monad](using
|
||||||
V: ValuesAlgebra[S, Alg],
|
V: ValuesAlgebra[S, Alg],
|
||||||
T: TypesAlgebra[S, Alg],
|
T: TypesAlgebra[S, Alg],
|
||||||
A: AbilitiesAlgebra[S, Alg],
|
A: AbilitiesAlgebra[S, Alg],
|
||||||
|
Loading…
x
Reference in New Issue
Block a user