fix(compiler): Generate stream restriction for scoped exprs [fixes LNG-222] (#841)

* Add show for AST

* Update ForSem

* Fix if and try

* Fix else, otherwise, catch, add tests

* Add integration tests
This commit is contained in:
InversionSpaces 2023-08-17 10:30:02 +04:00 committed by GitHub
parent f562bd40b6
commit eb4cdb0dd1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 409 additions and 94 deletions

View File

@ -182,7 +182,7 @@ lazy val parser = crossProject(JVMPlatform, JSPlatform)
"org.typelevel" %%% "cats-free" % catsV
)
)
.dependsOn(types)
.dependsOn(types, helpers)
lazy val linker = crossProject(JVMPlatform, JSPlatform)
.withoutSuffixFor(JVMPlatform)
@ -200,6 +200,7 @@ lazy val tree = crossProject(JVMPlatform, JSPlatform)
"org.typelevel" %%% "cats-free" % catsV
)
)
.dependsOn(helpers)
lazy val raw = crossProject(JVMPlatform, JSPlatform)
.withoutSuffixFor(JVMPlatform)
@ -212,7 +213,7 @@ lazy val model = crossProject(JVMPlatform, JSPlatform)
.withoutSuffixFor(JVMPlatform)
.crossType(CrossType.Pure)
.settings(commons)
.dependsOn(types, tree, raw)
.dependsOn(types, tree, raw, helpers)
lazy val res = crossProject(JVMPlatform, JSPlatform)
.withoutSuffixFor(JVMPlatform)
@ -228,7 +229,6 @@ lazy val inline = crossProject(JVMPlatform, JSPlatform)
.settings(commons)
.dependsOn(raw, model)
lazy val transform = crossProject(JVMPlatform, JSPlatform)
.withoutSuffixFor(JVMPlatform)
.crossType(CrossType.Pure)
@ -304,6 +304,18 @@ lazy val constants = crossProject(JVMPlatform, JSPlatform)
)
.dependsOn(parser, raw)
lazy val helpers = crossProject(JVMPlatform, JSPlatform)
.withoutSuffixFor(JVMPlatform)
.crossType(CrossType.Pure)
.in(file("utils/helpers"))
.settings(commons)
.settings(
libraryDependencies ++= Seq(
"org.typelevel" %%% "cats-core" % catsV,
"org.typelevel" %%% "cats-free" % catsV
)
)
lazy val `backend-air` = crossProject(JVMPlatform, JSPlatform)
.withoutSuffixFor(JVMPlatform)
.crossType(CrossType.Pure)

View File

@ -0,0 +1,83 @@
aqua StreamExports
export FailureSrv, streamIf, streamTry, streamFor, streamComplex
service FailureSrv("failure"):
fail(msg: string)
func streamIf() -> i8:
on HOST_PEER_ID:
if true:
stream: *i8
stream <<- 1
else:
stream: *i8
stream <<- 2
if false:
stream: *i8
stream <<- 3
else:
stream: *i8
stream <<- 4
stream: *i8
stream <<- 5
<- stream!
func streamTry() -> i8:
on HOST_PEER_ID:
try:
stream: *i8
stream <<- 1
FailureSrv.fail("try")
catch e:
stream: *i8
stream <<- 2
FailureSrv.fail("catch")
otherwise:
stream: *i8
stream <<- 3
stream: *i8
stream <<- 4
<- stream!
func streamFor() -> i8:
on HOST_PEER_ID:
for i <- [1, 2, 3]:
stream: *i8
stream <<- i
stream: *i8
stream <<- 4
<- stream!
func streamComplex() -> i8:
on HOST_PEER_ID:
for i <- [1, 2, 3]:
try:
if i == 2:
stream: *i8
stream <<- i
FailureSrv.fail("if")
else:
stream: *i8
stream <<- i
stream: *i8
stream <<- i + 3
catch e:
stream: *i8
stream <<- i + 6
stream: *i8
stream <<- i + 9
stream: *i8
stream <<- 13
<- stream!

View File

@ -39,6 +39,7 @@ import { coCall } from '../examples/coCall.js';
import { bugLNG60Call, passArgsCall } from '../examples/passArgsCall.js';
import { streamArgsCall } from '../examples/streamArgsCall.js';
import { streamResultsCall } from '../examples/streamResultsCall.js';
import { streamIfCall, streamForCall, streamTryCall, streamComplexCall } from '../examples/streamScopes.js';
import { pushToStreamCall } from '../examples/pushToStreamCall.js';
import { literalCall } from '../examples/returnLiteralCall.js';
import { multiReturnCall } from '../examples/multiReturnCall.js';
@ -158,6 +159,30 @@ describe('Testing examples', () => {
expect(streamResResult).toEqual([[], ['a', 'b', 'c']]);
});
it('streamScopes.aqua streamIf', async () => {
let streamIfResult = await streamIfCall();
expect(streamIfResult).toEqual(5);
});
it('streamScopes.aqua streamTry', async () => {
let streamTryResult = await streamTryCall();
expect(streamTryResult).toEqual(4);
});
it('streamScopes.aqua streamFor', async () => {
let streamTryResult = await streamForCall();
expect(streamTryResult).toEqual(4);
});
it('streamScopes.aqua streamComplex', async () => {
let streamTryResult = await streamComplexCall();
expect(streamTryResult).toEqual(13);
});
it('if.aqua', async () => {
await ifCall();
});

View File

@ -0,0 +1,35 @@
import {
streamIf,
streamTry,
streamFor,
streamComplex,
registerFailureSrv,
} from '../compiled/examples/streamScopes.js';
export async function streamIfCall() {
return await streamIf();
}
export async function streamTryCall() {
registerFailureSrv({
fail: (msg) => {
return Promise.reject(msg);
},
});
return await streamTry();
}
export async function streamForCall() {
return await streamFor();
}
export async function streamComplexCall() {
registerFailureSrv({
fail: (msg) => {
return Promise.reject(msg);
},
});
return await streamComplex();
}

View File

@ -9,12 +9,15 @@ import cats.syntax.apply.*
import scala.annotation.tailrec
import aqua.helpers.Tree
trait TreeNodeCompanion[T <: TreeNode[T]] {
given showTreeLabel: Show[T]
type Tree = Cofree[Chain, T]
// TODO: Use helpers.Tree istead of this function
private def showOffset(what: Tree, offset: Int): String = {
val spaces = "| " * offset
spaces + what.head.show + what.tail.map {
@ -98,8 +101,7 @@ trait TreeNodeCompanion[T <: TreeNode[T]] {
given Show[Tree] with
override def show(t: Tree): String =
showOffset(t, 0)
override def show(t: Tree): String = Tree.show(t)
given Show[(Tree, Tree)] with

View File

@ -4,10 +4,13 @@ import aqua.parser.expr.*
import aqua.parser.head.{HeadExpr, HeaderExpr}
import aqua.parser.lift.{LiftParser, Span}
import aqua.parser.lift.LiftParser.*
import aqua.helpers.Tree
import cats.data.{Chain, Validated, ValidatedNec}
import cats.free.Cofree
import cats.{Comonad, Eval}
import cats.~>
import cats.Show
case class Ast[S[_]](head: Ast.Head[S], tree: Ast.Tree[S]) {
@ -19,6 +22,16 @@ case class Ast[S[_]](head: Ast.Head[S], tree: Ast.Tree[S]) {
}
object Ast {
type Tree[S[_]] = Cofree[Chain, Expr[S]]
type Head[S[_]] = Cofree[Chain, HeaderExpr[S]]
type Tree[S[_]] = Cofree[Chain, Expr[S]]
given [S[_]]: Show[Ast[S]] with {
def show(ast: Ast[S]): String = {
val head = Tree.show(ast.head)
val body = Tree.show(ast.tree)
s"$head\n$body"
}
}
}

View File

@ -7,16 +7,18 @@ import aqua.parser.expr.func.ReturnExpr
import aqua.parser.lift.LiftParser.*
import aqua.parser.lift.Span.{P0ToSpan, PToSpan}
import aqua.parser.lift.{LiftParser, Span}
import aqua.parser.Ast.Tree
import aqua.parser.ListToTreeConverter
import cats.data.Chain.:==
import cats.data.{Chain, NonEmptyChain, Validated, ValidatedNec}
import cats.free.Cofree
import cats.Show
import cats.data.Validated.{invalid, invalidNec, invalidNel, valid, validNec, validNel}
import cats.parse.{Parser as P, Parser0 as P0}
import cats.syntax.comonad.*
import cats.{~>, Comonad, Eval}
import scribe.Logging
import aqua.parser.Ast.Tree
import aqua.parser.ListToTreeConverter
abstract class Expr[F[_]](val companion: Expr.Companion, val token: Token[F]) {
@ -109,4 +111,9 @@ object Expr {
.result
}
}
given [S[_]]: Show[Expr[S]] with {
// TODO: Make it better
def show(e: Expr[S]): String = e.toString
}
}

View File

@ -3,17 +3,19 @@ package aqua.parser.head
import aqua.parser.Ast
import aqua.parser.lexer.Token
import aqua.parser.lift.LiftParser
import cats.{Comonad, Eval}
import cats.data.Chain
import cats.free.Cofree
import cats.parse.Parser as P
import cats.~>
import aqua.parser.lift.Span
import aqua.parser.lift.Span.{P0ToSpan, PToSpan}
import cats.{Comonad, Eval}
import cats.data.Chain
import cats.free.Cofree
import cats.Show
import cats.parse.Parser as P
import cats.~>
trait HeaderExpr[S[_]] {
def token: Token[S]
def mapK[K[_]: Comonad](fk: S ~> K): HeaderExpr[K]
}
@ -30,4 +32,9 @@ object HeaderExpr {
override def ast: P[Ast.Head[Span.S]] =
p.map(Cofree[Chain, HeaderExpr[Span.S]](_, Eval.now(Chain.empty)))
}
given [S[_]]: Show[HeaderExpr[S]] with {
// TODO: Make it better
def show(e: HeaderExpr[S]): String = e.toString
}
}

View File

@ -22,26 +22,26 @@ class CatchSem[S[_]](val expr: CatchExpr[S]) extends AnyVal {
): Prog[Alg, Raw] =
Prog
.around(
N.beginScope(expr.name) >>
L.beginScope() >>
N.define(expr.name, ValueRaw.lastError.baseType),
N.define(expr.name, ValueRaw.lastError.baseType),
(_, g: Raw) =>
N.endScope() >> L.endScope() as (
g match {
case FuncOp(op) =>
TryTag.Catch
g match {
case FuncOp(op) =>
for {
restricted <- FuncOpSem.restrictStreamsInScope(op)
tag = TryTag.Catch
.wrap(
SeqTag.wrap(
AssignmentTag(ValueRaw.lastError, expr.name.value).leaf,
op
restricted
)
)
.toFuncOp
case _ =>
Raw.error("Wrong body of the `catch` expression")
}
)
} yield tag.toFuncOp
case _ =>
Raw.error("Wrong body of the `catch` expression").pure
}
)
.abilitiesScope[S](expr.token)
.namesScope(expr.token)
.locationsScope()
}

View File

@ -9,6 +9,7 @@ import aqua.semantics.rules.locations.LocationsAlgebra
import aqua.semantics.rules.names.NamesAlgebra
import cats.syntax.applicative.*
import cats.syntax.functor.*
import cats.Monad
class ElseOtherwiseSem[S[_]](val expr: ElseOtherwiseExpr[S]) extends AnyVal {
@ -19,20 +20,23 @@ class ElseOtherwiseSem[S[_]](val expr: ElseOtherwiseExpr[S]) extends AnyVal {
L: LocationsAlgebra[S, Alg]
): Prog[Alg, Raw] =
Prog
.after[Alg, Raw] {
case FuncOp(op) =>
expr.kind
.fold(
ifElse = IfTag.Else,
ifOtherwise = TryTag.Otherwise
)
.wrap(op)
.toFuncOp
.pure
case _ =>
val name = expr.kind.fold("`else`", "`otherwise`")
Raw.error(s"Wrong body of the $name expression").pure
}
.after((ops: Raw) =>
ops match {
case FuncOp(op) =>
for {
restricted <- FuncOpSem.restrictStreamsInScope(op)
tag = expr.kind
.fold(
ifElse = IfTag.Else,
ifOtherwise = TryTag.Otherwise
)
.wrap(restricted)
} yield tag.toFuncOp
case _ =>
val name = expr.kind.fold("`else`", "`otherwise`")
Raw.error(s"Wrong body of the $name expression").pure
}
)
.abilitiesScope(expr.token)
.namesScope(expr.token)
.locationsScope()

View File

@ -11,6 +11,7 @@ import aqua.semantics.rules.abilities.AbilitiesAlgebra
import aqua.semantics.rules.names.NamesAlgebra
import aqua.semantics.rules.types.TypesAlgebra
import aqua.types.{ArrayType, BoxType, StreamType}
import aqua.semantics.expr.func.FuncOpSem
import cats.Monad
import cats.data.Chain
@ -18,6 +19,7 @@ import cats.syntax.applicative.*
import cats.syntax.apply.*
import cats.syntax.flatMap.*
import cats.syntax.functor.*
import cats.syntax.option.*
class ForSem[S[_]](val expr: ForExpr[S]) extends AnyVal {
@ -29,49 +31,44 @@ class ForSem[S[_]](val expr: ForExpr[S]) extends AnyVal {
): Prog[F, Raw] =
Prog
.around(
V.valueToRaw(expr.iterable).flatMap[Option[ValueRaw]] {
V.valueToRaw(expr.iterable).flatMap {
case Some(vm) =>
vm.`type` match {
case t: BoxType =>
N.define(expr.item, t.element).as(Option(vm))
N.define(expr.item, t.element).as(vm.some)
case dt =>
T.ensureTypeMatches(expr.iterable, ArrayType(dt), dt).as(Option.empty[ValueRaw])
T.ensureTypeMatches(expr.iterable, ArrayType(dt), dt).as(none)
}
case _ => None.pure[F]
case _ => none.pure
},
(stOpt: Option[ValueRaw], ops: Raw) =>
N.streamsDefinedWithinScope()
.map(streams =>
(stOpt, ops) match {
case (Some(vm), FuncOp(op)) =>
val innerTag = expr.mode.fold(SeqTag) {
case ForExpr.Mode.ParMode => ParTag
case ForExpr.Mode.TryMode => TryTag
}
// Without type of ops specified
// scala compiler fails to compile this
(iterable, ops: Raw) =>
(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.collect { case ForExpr.Mode.ParMode => WaitMode }
val mode = expr.mode.collect { case ForExpr.Mode.ParMode => WaitMode }
val forTag =
ForTag(expr.item.value, vm, mode).wrap(
innerTag
.wrap(
// Restrict the streams created within this scope
streams.toList.foldLeft(op) { case (tree, (streamName, streamType)) =>
RestrictionTag(streamName, streamType).wrap(tree)
},
NextTag(expr.item.value).leaf
)
)
val forTag = ForTag(expr.item.value, vm, mode).wrap(
innerTag.wrap(
restricted,
NextTag(expr.item.value).leaf
)
)
// 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
case _ =>
Raw.error("Wrong body of the `for` expression")
// 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
}
)
case _ => Raw.error("Wrong body of the `for` expression").pure[F]
}
)
.namesScope[S](expr.token)
.abilitiesScope[S](expr.token)
.namesScope(expr.token)
.abilitiesScope(expr.token)
}

View File

@ -0,0 +1,22 @@
package aqua.semantics.expr.func
import cats.Monad
import cats.syntax.functor.*
import aqua.semantics.rules.names.NamesAlgebra
import aqua.raw.Raw
import aqua.raw.ops.{RawTag, RestrictionTag}
object FuncOpSem {
def restrictStreamsInScope[S[_], Alg[_]: Monad](tree: RawTag.Tree)(using
N: NamesAlgebra[S, Alg]
): Alg[RawTag.Tree] = N
.streamsDefinedWithinScope()
.map(streams =>
streams.toList
.foldLeft(tree) { case (tree, (streamName, streamType)) =>
RestrictionTag(streamName, streamType).wrap(tree)
}
)
}

View File

@ -41,17 +41,18 @@ class IfSem[S[_]](val expr: IfExpr[S]) extends AnyVal {
).map(Option.when(_)(raw))
)
),
(value: Option[ValueRaw], ops: Raw) =>
value
.fold(
Raw.error("`if` expression errored in matching types")
)(raw =>
ops match {
case FuncOp(op) => IfTag(raw).wrap(op).toFuncOp
case _ => Raw.error("Wrong body of the `if` expression")
}
)
.pure
// Without type of ops specified
// scala compiler fails to compile this
(value, ops: Raw) =>
(value, ops) match {
case (Some(vr), FuncOp(op)) =>
for {
restricted <- FuncOpSem.restrictStreamsInScope(op)
tag = IfTag(vr).wrap(restricted)
} yield tag.toFuncOp
case (None, _) => Raw.error("`if` expression errored in matching types").pure
case _ => Raw.error("Wrong body of the `if` expression").pure
}
)
.abilitiesScope[S](expr.token)
.namesScope[S](expr.token)

View File

@ -14,6 +14,7 @@ import cats.data.Chain
import cats.syntax.applicative.*
import cats.syntax.apply.*
import cats.syntax.flatMap.*
import cats.syntax.traverse.*
import cats.syntax.functor.*
import cats.{Monad, Traverse}
@ -27,8 +28,8 @@ class OnSem[S[_]](val expr: OnExpr[S]) extends AnyVal {
Prog.around(
(
V.ensureIsString(expr.peerId),
Traverse[List]
.traverse(expr.via)(v =>
expr.via
.traverse(v =>
V.valueToRaw(v).flatTap {
case Some(vm) =>
vm.`type` match {

View File

@ -9,7 +9,9 @@ import aqua.semantics.rules.abilities.AbilitiesAlgebra
import aqua.semantics.rules.locations.LocationsAlgebra
import aqua.semantics.rules.names.NamesAlgebra
import aqua.semantics.rules.types.TypesAlgebra
import cats.syntax.applicative.*
import cats.syntax.functor.*
import cats.Monad
class TrySem[S[_]](val expr: TryExpr[S]) extends AnyVal {
@ -20,12 +22,19 @@ class TrySem[S[_]](val expr: TryExpr[S]) extends AnyVal {
L: LocationsAlgebra[S, Alg]
): Prog[Alg, Raw] =
Prog
.after[Alg, Raw] {
case FuncOp(o) =>
TryTag.wrap(o).toFuncOp.pure[Alg]
case _ =>
Raw.error("Wrong body of the `try` expression").pure[Alg]
}
// Without type of ops specified
// scala compiler fails to compile this
.after((ops: Raw) =>
ops match {
case FuncOp(op) =>
for {
restricted <- FuncOpSem.restrictStreamsInScope(op)
tag = TryTag.wrap(restricted)
} yield tag.toFuncOp
case _ =>
Raw.error("Wrong body of the `try` expression").pure[Alg]
}
)
.abilitiesScope(expr.token)
.namesScope(expr.token)
.locationsScope()

View File

@ -5,7 +5,7 @@ import aqua.parser.Ast
import aqua.raw.ops.{Call, CallArrowRawTag, FuncOp, OnTag, ParTag, RawTag, SeqGroupTag, SeqTag}
import aqua.parser.Parser
import aqua.parser.lift.{LiftParser, Span}
import aqua.raw.value.{ApplyBinaryOpRaw, LiteralRaw, ValueRaw}
import aqua.raw.value.{ApplyBinaryOpRaw, LiteralRaw, ValueRaw, VarRaw}
import aqua.types.*
import aqua.raw.ops.*
@ -72,6 +72,24 @@ class SemanticsSpec extends AnyFlatSpec with Matchers with Inside {
def neq(left: ValueRaw, right: ValueRaw): ApplyBinaryOpRaw =
ApplyBinaryOpRaw(ApplyBinaryOpRaw.Op.Neq, left, right)
def declareStreamPush(
name: String,
value: String
): RawTag.Tree = {
val streamType = StreamType(ScalarType.string)
val stream = VarRaw(name, streamType)
RestrictionTag(stream.name, streamType).wrap(
SeqTag.wrap(
DeclareStreamTag(stream).leaf,
PushToStreamTag(
LiteralRaw.quote(value),
Call.Export(name, streamType)
).leaf
)
)
}
// use it to fix https://github.com/fluencelabs/aqua/issues/90
"semantics" should "create right model" in {
val script =
@ -459,4 +477,53 @@ class SemanticsSpec extends AnyFlatSpec with Matchers with Inside {
body.equalsOrShowDiff(expected) should be(true)
}
}
it should "restrict streams inside `if`" in {
val script = """
|func test():
| if "a" != "b":
| stream: *string
| stream <<- "a"
| else:
| stream: *string
| stream <<- "b"
|""".stripMargin
insideBody(script) { body =>
val expected = IfTag(neq(LiteralRaw.quote("a"), LiteralRaw.quote("b"))).wrap(
declareStreamPush("stream", "a"),
declareStreamPush("stream", "b")
)
body.equalsOrShowDiff(expected) should be(true)
}
}
it should "restrict streams inside `try`" in {
val script = """
|func test():
| try:
| stream: *string
| stream <<- "a"
| catch e:
| stream: *string
| stream <<- "b"
| otherwise:
| stream: *string
| stream <<- "c"
|""".stripMargin
insideBody(script) { body =>
val expected = TryTag.wrap(
declareStreamPush("stream", "a"),
SeqTag.wrap(
AssignmentTag(ValueRaw.lastError, "e").leaf,
declareStreamPush("stream", "b")
),
declareStreamPush("stream", "c")
)
body.equalsOrShowDiff(expected) should be(true)
}
}
}

View File

@ -0,0 +1,30 @@
package aqua.helpers
import cats.data.Chain
import cats.free.Cofree
import cats.Traverse
import cats.Show
import cats.Eval
import cats.syntax.show.*
import cats.syntax.traverse.*
import cats.syntax.foldable.*
object Tree {
def show[F[_]: Traverse, A: Show](
what: Cofree[F, A]
): String =
Cofree
.cata[F, A, List[String]](what) { case (head, tail) =>
Eval.later {
val children = tail.combineAll.map("| " + _)
val parent = head.show
if (children.isEmpty) List(parent)
else (parent + ":") +: children
}
}
.value
.mkString("\n")
}