refactor(semantics): Refactor semantics to produce concrete tags [fixes LNG-201] (#776)

* Introduce IfTag, TryTag; Remove XorTag

* Add IfTag, TryTag inlining

* Fix test compilation

* Fix test

* Hack to fix topology

* Support try otherwise syntax

* Add comments

* Refactor diff show

* Handle ParTag.Par in single check, add tests
This commit is contained in:
InversionSpaces 2023-06-29 18:20:47 +02:00 committed by GitHub
parent 339d3a8217
commit 8ba7021cd4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
34 changed files with 1079 additions and 348 deletions

View File

@ -1,7 +1,8 @@
package aqua.lsp
import aqua.parser.lexer.Token
import aqua.semantics.rules.{ReportError, StackInterpreter}
import aqua.semantics.rules.StackInterpreter
import aqua.semantics.rules.errors.ReportErrors
import aqua.semantics.rules.locations.{LocationsAlgebra, LocationsState}
import cats.data.State
import monocle.Lens
@ -10,7 +11,7 @@ import scribe.Logging
class LocationsInterpreter[S[_], X](implicit
lens: Lens[X, LocationsState[S]],
error: ReportError[S, X]
error: ReportErrors[S, X]
) extends LocationsAlgebra[S, State[X, *]] with Logging {
type SX[A] = State[X, A]

View File

@ -3,9 +3,9 @@ package aqua.lsp
import aqua.parser.Ast
import aqua.parser.head.{ImportExpr, ImportFromExpr, UseExpr, UseFromExpr}
import aqua.parser.lexer.{LiteralToken, Token}
import aqua.semantics.rules.ReportError
import aqua.semantics.rules.errors.ReportErrors
import aqua.semantics.rules.locations.LocationsState
import aqua.semantics.{CompilerState, RulesViolated, SemanticError, Semantics}
import aqua.semantics.{CompilerState, RawSemantics, RulesViolated, SemanticError, Semantics}
import cats.data.Validated.{Invalid, Valid}
import cats.syntax.applicative.*
import cats.syntax.apply.*
@ -57,14 +57,14 @@ class LspSemantics[S[_]] extends Semantics[S, LspContext[S]] {
GenLens[CompilerState[S]](_.locations)
import monocle.syntax.all.*
implicit val re: ReportError[S, CompilerState[S]] =
implicit val re: ReportErrors[S, CompilerState[S]] =
(st: CompilerState[S], token: Token[S], hints: List[String]) =>
st.focus(_.errors).modify(_.append(RulesViolated(token, hints)))
implicit val locationsInterpreter: LocationsInterpreter[S, CompilerState[S]] =
new LocationsInterpreter[S, CompilerState[S]]()
Semantics
RawSemantics
.interpret(ast, initState, init.raw)
.map { case (state, ctx) =>
NonEmptyChain

View File

@ -10,12 +10,15 @@ import aqua.raw.value.*
import aqua.types.{ArrayType, ArrowType, BoxType, CanonStreamType, StreamType}
import cats.syntax.traverse.*
import cats.syntax.applicative.*
import cats.syntax.flatMap.*
import cats.syntax.apply.*
import cats.syntax.functor.*
import cats.syntax.option.*
import cats.instances.list.*
import cats.data.{Chain, State, StateT}
import cats.syntax.show.*
import scribe.{log, Logging}
import aqua.model.inline.Inline.parDesugarPrefixOpt
/**
* [[TagInliner]] prepares a [[RawTag]] for futher processing by converting [[ValueRaw]]s into [[ValueModel]]s.
@ -30,13 +33,66 @@ object TagInliner extends Logging {
import RawValueInliner.{callToModel, valueListToModel, valueToModel}
import Inline.*
import Inline.parDesugarPrefix
private def pure[S](op: OpModel): State[S, (Option[OpModel], Option[OpModel.Tree])] =
State.pure(Some(op) -> None)
/**
* Result of [[RawTag]] inlining
*
* @param prefix Previous instructions
*/
enum TagInlined(prefix: Option[OpModel.Tree]) {
private def none[S]: State[S, (Option[OpModel], Option[OpModel.Tree])] =
State.pure(None -> None)
/**
* Tag inlining emitted nothing
*/
case Empty(
prefix: Option[OpModel.Tree] = None
) extends TagInlined(prefix)
/**
* Tag inlining emitted one parent model
*
* @param model Model which will wrap children
*/
case Single(
model: OpModel,
prefix: Option[OpModel.Tree] = None
) extends TagInlined(prefix)
/**
* Tag inling emitted complex transformation
*
* @param toModel Function from children results to result of this tag
*/
case Mapping(
toModel: Chain[OpModel.Tree] => OpModel.Tree,
prefix: Option[OpModel.Tree] = None
) extends TagInlined(prefix)
/**
* Finalize inlining, construct a tree
*
* @param children Children results
* @return Result of inlining
*/
def build(children: Chain[OpModel.Tree]): OpModel.Tree = {
val inlined = this match {
case Empty(_) => children
case Single(model, _) =>
Chain.one(model.wrap(children))
case Mapping(toModel, _) =>
Chain.one(toModel(children))
}
SeqModel.wrap(Chain.fromOption(prefix) ++ inlined)
}
}
private def pure[S](op: OpModel): State[S, TagInlined] =
TagInlined.Single(model = op).pure
private def none[S]: State[S, TagInlined] =
TagInlined.Empty().pure
private def combineOpsWithSeq(l: Option[OpModel.Tree], r: Option[OpModel.Tree]) =
l match {
@ -120,7 +176,7 @@ object TagInliner extends Logging {
def tagToModel[S: Mangler: Arrows: Exports](
tag: RawTag,
treeFunctionName: String
): State[S, (Option[OpModel], Option[OpModel.Tree])] =
): State[S, TagInlined] =
tag match {
case OnTag(peerId, via) =>
for {
@ -133,21 +189,52 @@ object TagInliner extends Logging {
viaD = Chain.fromSeq(viaDeFlattened.map(_._1))
viaF = viaDeFlattened.flatMap(_._2)
} yield Some(OnModel(pid, viaD)) -> parDesugarPrefix(viaF.prependedAll(pif))
case MatchMismatchTag(left, right, shouldMatch) =>
for {
ld <- valueToModel(left)
rd <- valueToModel(right)
ldCanon <- canonicalizeIfStream(ld._1, ld._2)
rdCanon <- canonicalizeIfStream(rd._1, rd._2)
} yield Some(
MatchMismatchModel(ldCanon._1, rdCanon._1, shouldMatch)
) -> parDesugarPrefixOpt(
ldCanon._2,
rdCanon._2
} yield TagInlined.Single(
model = OnModel(pid, viaD),
prefix = parDesugarPrefix(viaF.prependedAll(pif))
)
case IfTag(leftRaw, rightRaw, shouldMatch) =>
(
valueToModel(leftRaw) >>= canonicalizeIfStream.tupled,
valueToModel(rightRaw) >>= canonicalizeIfStream.tupled
).mapN { case ((leftModel, leftPrefix), (rightModel, rightPrefix)) =>
val prefix = parDesugarPrefixOpt(leftPrefix, rightPrefix)
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
MatchMismatchModel(
leftModel,
rightModel,
shouldMatch
).wrap(ifBody) +: elseBodyAugmented
}.getOrElse(children)
)
TagInlined.Mapping(
toModel = toModel,
prefix = prefix
)
}
case TryTag => pure(XorModel)
case ForTag(item, iterable, mode) =>
for {
vp <- valueToModel(iterable)
@ -164,18 +251,21 @@ object TagInliner extends Logging {
iterable.`type`
}
_ <- Exports[S].resolved(item, VarModel(n, elementType))
} yield {
val m = mode.map {
m = mode.map {
case ForTag.WaitMode => ForModel.NeverMode
case ForTag.PassMode => ForModel.NullMode
}
Some(ForModel(n, v, m)) -> p
}
} yield TagInlined.Single(
model = ForModel(n, v, m),
prefix = p
)
case PushToStreamTag(operand, exportTo) =>
valueToModel(operand).map { case (v, p) =>
Some(PushToStreamModel(v, CallModel.callExport(exportTo))) -> p
TagInlined.Single(
model = PushToStreamModel(v, CallModel.callExport(exportTo)),
prefix = p
)
}
case CanonicalizeTag(operand, exportTo) =>
@ -184,9 +274,14 @@ object TagInliner extends Logging {
case (l @ LiteralModel(_, _), p) =>
for {
_ <- Exports[S].resolved(exportTo.name, l)
} yield None -> p
} yield TagInlined.Empty(prefix = p)
case (v, p) =>
State.pure(Some(CanonicalizeModel(v, CallModel.callExport(exportTo))) -> p)
TagInlined
.Single(
model = CanonicalizeModel(v, CallModel.callExport(exportTo)),
prefix = p
)
.pure
}
case FlattenTag(operand, assignTo) =>
@ -195,9 +290,14 @@ object TagInliner extends Logging {
case (l @ LiteralModel(_, _), p) =>
for {
_ <- Exports[S].resolved(assignTo, l)
} yield None -> p
} yield TagInlined.Empty(prefix = p)
case (v, p) =>
State.pure(Some(FlattenModel(v, assignTo)) -> p)
TagInlined
.Single(
model = FlattenModel(v, assignTo),
prefix = p
)
.pure
}
case JoinTag(operands) =>
@ -205,13 +305,19 @@ object TagInliner extends Logging {
.traverse(o => valueToModel(o))
.map(nel => {
logger.trace("join after " + nel.map(_._1))
// None because join behaviour will be processed in ApplyPropertiesRawInliner
None -> parDesugarPrefix(nel.toList.flatMap(_._2))
// Empty because join behaviour will be processed in ApplyPropertiesRawInliner
TagInlined.Empty(prefix = parDesugarPrefix(nel.toList.flatMap(_._2)))
})
case CallArrowRawTag(exportTo, value: CallArrowRaw) =>
CallArrowRawInliner.unfoldArrow(value, exportTo).flatMap { case (_, inline) =>
RawValueInliner.inlineToTree(inline).map(tree => (None, Some(SeqModel.wrap(tree: _*))))
RawValueInliner
.inlineToTree(inline)
.map(tree =>
TagInlined.Empty(
prefix = SeqModel.wrap(tree).some
)
)
}
case AssignmentTag(value, assignTo) =>
@ -226,43 +332,41 @@ object TagInliner extends Logging {
// NOTE: Name <assignTo> should not exist yet
_ <- Mangler[S].forbidName(assignTo)
_ <- Exports[S].resolved(assignTo, model)
} yield None -> prefix
} yield TagInlined.Empty(prefix = prefix)
}
case ClosureTag(arrow, detach) =>
if (detach) Arrows[S].resolved(arrow, None).map(_ => None -> None)
if (detach) Arrows[S].resolved(arrow, None).as(TagInlined.Empty())
else
for {
t <- Mangler[S].findAndForbidName(arrow.name)
_ <- Arrows[S].resolved(arrow, Some(t))
} yield Some(CaptureTopologyModel(t)) -> None
} yield TagInlined.Single(model = CaptureTopologyModel(t))
case NextTag(item) =>
for {
exps <- Exports[S].exports
} yield {
exps.get(item).collect { case VarModel(n, _, _) =>
model = exps.get(item).collect { case VarModel(n, _, _) =>
NextModel(n)
} -> None
}
} yield model.fold(TagInlined.Empty())(m => TagInlined.Single(model = m))
case RestrictionTag(name, isStream) =>
pure(RestrictionModel(name, isStream))
case _: SeqGroupTag => pure(SeqModel)
case ParTag.Detach => pure(DetachModel)
case _: ParGroupTag => pure(ParModel)
case XorTag | XorTag.LeftBiased =>
pure(XorModel)
case DeclareStreamTag(value) =>
value match
case VarRaw(name, _) =>
for {
cd <- valueToModel(value)
_ <- Exports[S].resolved(name, cd._1)
} yield None -> cd._2
} yield TagInlined.Empty(prefix = cd._2)
case _ => none
case _: SeqGroupTag => pure(SeqModel)
case ParTag.Detach => pure(DetachModel)
case _: ParGroupTag => pure(ParModel)
case _: NoExecTag => none
case _ =>
logger.warn(s"Tag $tag must have been eliminated at this point")
@ -271,16 +375,13 @@ object TagInliner extends Logging {
private def traverseS[S](
cf: RawTag.Tree,
f: RawTag => State[S, (Option[OpModel], Option[OpModel.Tree])]
f: RawTag => State[S, TagInlined]
): State[S, OpModel.Tree] =
for {
headTree <- f(cf.head)
headInlined <- f(cf.head)
tail <- StateT.liftF(cf.tail)
tailTree <- tail.traverse(traverseS[S](_, f))
} yield headTree match {
case (Some(m), prefix) => SeqModel.wrap(prefix.toList :+ m.wrap(tailTree.toList: _*): _*)
case (None, prefix) => SeqModel.wrap(prefix.toList ++ tailTree.toList: _*)
}
children <- tail.traverse(traverseS[S](_, f))
} yield headInlined.build(children)
def handleTree[S: Exports: Mangler: Arrows](
tree: RawTag.Tree,

View File

@ -8,13 +8,15 @@ import aqua.types.{ScalarType, StreamType}
import org.scalatest.flatspec.AnyFlatSpec
import org.scalatest.matchers.should.Matchers
import cats.syntax.show.*
import org.scalatest.Inside
import aqua.model.inline.TagInliner.TagInlined
class TagInlinerSpec extends AnyFlatSpec with Matchers {
class TagInlinerSpec extends AnyFlatSpec with Matchers with Inside {
"CanonicalizeTag" should "pass literals as is" in {
val canonTo = "canon_to"
val model = TagInliner
val (state, inlined) = TagInliner
.tagToModel[InliningState](
CanonicalizeTag(ValueRaw.Nil, Call.Export(canonTo, StreamType(ScalarType.string))),
""
@ -22,15 +24,20 @@ class TagInlinerSpec extends AnyFlatSpec with Matchers {
.run(InliningState())
.value
model._1.resolvedExports(canonTo) shouldBe LiteralModel(ValueRaw.Nil.value, ValueRaw.Nil.baseType)
model._2._1 shouldBe None
model._2._2 shouldBe None
state.resolvedExports(canonTo) shouldBe LiteralModel(
ValueRaw.Nil.value,
ValueRaw.Nil.baseType
)
inside(inlined) { case TagInlined.Empty(prefix) =>
prefix shouldBe None
}
}
"FlattenTag" should "pass literals as is" in {
val canonTo = "canon_to"
val model = TagInliner
val (state, inlined) = TagInliner
.tagToModel[InliningState](
FlattenTag(ValueRaw.Nil, canonTo),
""
@ -38,11 +45,13 @@ class TagInlinerSpec extends AnyFlatSpec with Matchers {
.run(InliningState())
.value
model._1.resolvedExports(canonTo) shouldBe LiteralModel(
state.resolvedExports(canonTo) shouldBe LiteralModel(
ValueRaw.Nil.value,
ValueRaw.Nil.baseType
)
model._2._1 shouldBe None
model._2._2 shouldBe None
inside(inlined) { case TagInlined.Empty(prefix) =>
prefix shouldBe None
}
}
}

View File

@ -16,8 +16,4 @@ import cats.Monad
import cats.data.State
import cats.data.StateT
case class FuncOp(tree: RawTag.Tree) extends Raw {
def isRightAssoc: Boolean = RawTag.isRightAssoc(tree.head)
def :+:(prev: FuncOp): FuncOp = FuncOp(RawTag.rightAssocCombine(prev.tree, tree))
}
case class FuncOp(tree: RawTag.Tree) extends Raw

View File

@ -60,19 +60,51 @@ sealed trait ParGroupTag extends GroupTag
case object SeqTag extends SeqGroupTag {
override def wrap(children: Tree*): Tree =
super.wrapNonEmpty(children.filterNot(_.head == EmptyTag).toList, RawTag.empty)
override def wrap(children: Chain[Tree]): Tree =
super.wrapNonEmpty(children.filterNot(_.head == EmptyTag), RawTag.empty)
}
case object ParTag extends ParGroupTag {
case object Detach extends ParGroupTag
/**
* Used for `co` instruction
*/
case object Detach extends GroupTag
/**
* This tag should be eliminated in semantics
* and merged with [[ParTag]]
*
* Used for `par` instruction
*/
case object Par extends GroupTag
}
case object XorTag extends GroupTag {
case object LeftBiased extends GroupTag
case class IfTag(left: ValueRaw, right: ValueRaw, equal: Boolean) extends GroupTag
object IfTag {
/**
* This tag should be eliminated in semantics
* and merged with [[IfTag]]
*/
case object Else extends GroupTag
}
case class XorParTag(xor: RawTag.Tree, par: RawTag.Tree) extends RawTag
case object TryTag extends GroupTag {
/**
* This tag should be eliminated in semantics
* and merged with [[TryTag]]
*/
case object Catch extends GroupTag
/**
* This tag should be eliminated in semantics
* and merged with [[TryTag]]
*/
case object Otherwise extends GroupTag
}
case class OnTag(peerId: ValueRaw, via: Chain[ValueRaw]) extends SeqGroupTag {
@ -97,13 +129,6 @@ case class RestrictionTag(name: String, isStream: Boolean) extends SeqGroupTag {
copy(name = map.getOrElse(name, name))
}
case class MatchMismatchTag(left: ValueRaw, right: ValueRaw, shouldMatch: Boolean)
extends SeqGroupTag {
override def mapValues(f: ValueRaw => ValueRaw): RawTag =
MatchMismatchTag(left.map(f), right.map(f), shouldMatch)
}
case class ForTag(item: String, iterable: ValueRaw, mode: Option[ForTag.Mode] = None)
extends SeqGroupTag {

View File

@ -9,92 +9,34 @@ import cats.syntax.semigroup.*
trait RawTagGivens {
def isRightAssoc(tag: RawTag): Boolean = tag match {
case XorTag | ParTag => true
case _: XorParTag => true
case _ => false
}
// convert some tags in tree to fix corner cases
def fixCornerCases(tree: RawTag.Tree): RawTag.Tree =
Cofree
.cata[Chain, RawTag, RawTag.Tree](tree) {
case (XorParTag(left, right), _) =>
Eval.now(
ParTag.wrap(
XorTag.wrap(left),
right
)
)
case (XorTag.LeftBiased, tail) =>
// TODO: fix me in topology
// https://linear.app/fluence/issue/LNG-69/if-inside-on-produces-invalid-topology
Eval.now(
Cofree(
XorTag,
Eval.now(
tail.append(
CallArrowRawTag.service(LiteralRaw.quote("op"), "noop", Call(Nil, Nil)).leaf
)
)
)
)
case (head, tail) => Eval.now(Cofree(head, Eval.now(tail)))
}
.value
given Semigroup[RawTag.Tree] with
override def combine(x: RawTag.Tree, y: RawTag.Tree): RawTag.Tree = {
// Remove right-asscoc protection of Seq with single child
// Remove seq with single child
val flatX = SeqGroupTag.ungroupSingle(x)
val flatY = SeqGroupTag.ungroupSingle(y)
(flatX.head, flatY.head) match {
case (_, XorParTag(xor, par)) => combine(combine(flatX, xor), par)
case (XorParTag(xor, par), _) => combine(combine(xor, par), flatY)
case (SeqTag, SeqTag) => flatY.copy(tail = (flatX.tail, flatY.tail).mapN(_ ++ _))
case (SeqTag, SeqTag) => flatX.copy(tail = (flatX.tail, flatY.tail).mapN(_ ++ _))
case (_, SeqTag) => flatY.copy(tail = flatY.tail.map(_.prepend(flatX)))
case (SeqTag, _) => flatX.copy(tail = flatX.tail.map(_.append(flatY)))
case _ => SeqTag.wrap(flatX, flatY)
}
}
// Semigroup for foldRight processing
def rightAssocCombine(x: RawTag.Tree, y: RawTag.Tree): RawTag.Tree =
(x.head, y.head) match {
case (_: ParGroupTag, ParTag) =>
y.copy(tail = (x.tail, y.tail).mapN(_ ++ _))
case (XorTag, XorTag) =>
y.copy(tail = (x.tail, y.tail).mapN(_ ++ _))
case (XorTag.LeftBiased, XorTag) =>
SeqGroupTag.wrap(y.copy(tail = (x.tail, y.tail).mapN(_ ++ _)))
case (XorTag, ParTag) => XorParTag(x, y).leaf
case (_, ParTag | XorTag) =>
// When right-associative tag is combined with left-associative,
// we need result to be left-associative to prevent greedy behavior.
// SeqGroupTag does just this.
SeqGroupTag.wrap(y.copy(tail = y.tail.map(_.prepend(x))))
case (_, XorParTag(xor, par)) =>
rightAssocCombine(rightAssocCombine(x, xor), par)
case _ => x |+| y
}
extension (tree: RawTag.Tree)
def toFuncOp: FuncOp = FuncOp(tree)
def rename(vals: Map[String, String]): RawTag.Tree =
if (vals.isEmpty) tree
else
tree.map[RawTag](_.mapValues(_.renameVars(vals)).renameExports(vals))
else tree.map(_.mapValues(_.renameVars(vals)).renameExports(vals))
def renameExports(vals: Map[String, String]): RawTag.Tree =
if (vals.isEmpty) tree
else
tree.map[RawTag](_.renameExports(vals))
else tree.map(_.renameExports(vals))
def definesVarNames: Eval[Set[String]] =
Cofree.cata[Chain, RawTag, Set[String]](tree) { case (tag, acc) =>
Cofree.cata(tree) { case (tag, acc) =>
Eval.later(acc.foldLeft(tag.definesVarNames)(_ ++ _))
}
}

View File

@ -31,7 +31,7 @@ object FuncArrow {
): FuncArrow =
FuncArrow(
raw.name,
RawTag.fixCornerCases(raw.arrow.body),
raw.arrow.body,
raw.arrow.`type`,
raw.arrow.ret,
arrows,

View File

@ -71,13 +71,8 @@ sealed trait ParGroupModel extends GroupOpModel
case object SeqModel extends SeqGroupModel {
override def wrap(children: Tree*): Tree =
super.wrapNonEmpty(children.filterNot(_.head == EmptyModel).toList, EmptyModel.leaf)
// EmptyModel allowed useful for tests
def wrapWithEmpty(children: Tree*): Tree =
super.wrapNonEmpty(children.toList, EmptyModel.leaf)
override def wrap(children: Chain[Tree]): Tree =
super.wrapNonEmpty(children.filterNot(_.head == EmptyModel), EmptyModel.leaf)
}
case object ParModel extends ParGroupModel

View File

@ -1,6 +1,7 @@
package aqua.tree
import cats.data.Chain
import cats.data.Chain.*
import cats.free.Cofree
import cats.Eval
@ -16,10 +17,10 @@ trait TreeNode[T <: TreeNode[T]] {
def wrap(children: Chain[Tree]): Tree = Cofree(self, Eval.now(children))
protected def wrapNonEmpty(children: List[Tree], empty: Tree): Tree = children match {
case Nil => empty
case x :: Nil => x
case _ => Cofree(self, Eval.now(Chain.fromSeq(children)))
protected def wrapNonEmpty(children: Chain[Tree], empty: Tree): Tree = children match {
case Chain.nil => empty
case x ==: Chain.nil => x
case _ => Cofree(self, Eval.now(children))
}
}

View File

@ -24,35 +24,34 @@ trait TreeNodeCompanion[T <: TreeNode[T]] {
}.value
}
private def showDiffOffset(what: (Tree, Tree), offset: Int): String = {
private def showDiffOffset(left: Tree, right: Tree, offset: Int): String = {
val spaces = "| " * offset
val leftShow = left.head.show
val rightShow = right.head.show
val head =
if (what._1.head == what._2.head) what._1.head.show
if (leftShow == rightShow) leftShow
else {
val lft = what._1.head.show
val rgt = what._2.head.show
val commonPrefixLen = lft.zip(rgt).takeWhile(_ == _).length
val commonSuffixLen = rgt.reverse.zip(lft.reverse).takeWhile(_ == _).length
val commonPrefix = lft.take(commonPrefixLen)
val commonSuffix = rgt.takeRight(commonSuffixLen)
val lSuffix = lft.length - commonSuffixLen
val lftDiff =
if (commonPrefixLen - lSuffix < lft.length) lft.substring(commonPrefixLen, lSuffix)
else ""
val rSuffix = rgt.length - commonSuffixLen
val rgtDiff =
if (commonPrefixLen + rSuffix < rgt.length) rgt.substring(commonPrefixLen, rSuffix)
else ""
val commonPrefix = (l: String, r: String) =>
l.lazyZip(r).takeWhile(_ == _).map(_._1).mkString
val prefix = commonPrefix(leftShow, rightShow)
val suffix = commonPrefix(leftShow.reverse, rightShow.reverse).reverse
val diff = (s: String) => s.drop(prefix.length).dropRight(suffix.length)
val lftDiff = diff(leftShow)
val rgtDiff = diff(rightShow)
if (rgtDiff.isEmpty) {
commonPrefix + Console.YELLOW + lftDiff + Console.RESET + commonSuffix
prefix + Console.YELLOW + lftDiff + Console.RESET + suffix
} else {
commonPrefix +
Console.YELLOW + lftDiff + Console.RED + " != " + Console.CYAN + rgtDiff + Console.RESET + commonSuffix
prefix + Console.YELLOW + lftDiff + Console.RED +
" != " + Console.CYAN + rgtDiff + Console.RESET + suffix
}
}
spaces + head + (what._1.tail, what._2.tail).mapN {
spaces + head + (left.tail, right.tail).mapN {
case (c1, c2) if c1.isEmpty && c2.isEmpty => "\n"
case (c1, c2) =>
@tailrec
@ -62,7 +61,7 @@ trait TreeNodeCompanion[T <: TreeNode[T]] {
case (Nil, y :: tail) =>
nxt(tail, Nil, acc :+ (Console.CYAN + showOffset(y, offset + 1) + Console.RESET))
case (x :: xt, y :: yt) if x.head == y.head =>
nxt(xt, yt, acc :+ showDiffOffset(x -> y, offset + 1))
nxt(xt, yt, acc :+ showDiffOffset(x, y, offset + 1))
case (x :: xt, yt) if yt.exists(_.head == x.head) =>
val yh = yt.takeWhile(_.head != x.head)
nxt(
@ -82,7 +81,7 @@ trait TreeNodeCompanion[T <: TreeNode[T]] {
)
)
case (x :: xt, y :: yt) =>
nxt(xt, yt, acc :+ showDiffOffset(x -> y, offset + 1))
nxt(xt, yt, acc :+ showDiffOffset(x, y, offset + 1))
case (Nil, Nil) => acc.toList
}
@ -105,7 +104,7 @@ trait TreeNodeCompanion[T <: TreeNode[T]] {
given Show[(Tree, Tree)] with
override def show(tt: (Tree, Tree)): String =
showDiffOffset(tt, 0)
showDiffOffset(tt._1, tt._2, 0)
extension (t: Tree)

View File

@ -8,17 +8,35 @@ import aqua.parser.lift.LiftParser
import aqua.parser.lift.LiftParser.*
import cats.parse.Parser
import cats.{~>, Comonad}
import cats.syntax.comonad.*
import cats.syntax.functor.*
import aqua.parser.lift.Span
import aqua.parser.lift.Span.{P0ToSpan, PToSpan}
case class ElseOtherwiseExpr[F[_]](point: Token[F]) extends Expr[F](ElseOtherwiseExpr, point) {
override def mapK[K[_]: Comonad](fk: F ~> K): ElseOtherwiseExpr[K] = copy(point.mapK(fk))
case class ElseOtherwiseExpr[F[_]](kind: ElseOtherwiseExpr.Kind, point: Token[F])
extends Expr[F](ElseOtherwiseExpr, point) {
override def mapK[K[_]: Comonad](fk: F ~> K): ElseOtherwiseExpr[K] =
copy(point = point.mapK(fk))
}
object ElseOtherwiseExpr extends Expr.AndIndented {
enum Kind {
case Else, Otherwise
def fold[A](ifElse: => A, ifOtherwise: => A): A = this match {
case Else => ifElse
case Otherwise => ifOtherwise
}
}
override def validChildren: List[Expr.Lexem] = ForExpr.validChildren
override val p: Parser[ElseOtherwiseExpr[Span.S]] =
(`else` | `otherwise`).lift.map(Token.lift[Span.S, Unit](_)).map(ElseOtherwiseExpr(_))
(`else`.as(Kind.Else) | `otherwise`.as(Kind.Otherwise)).lift
.fproduct(span => Token.lift(span.as(())))
.map { case (kind, point) =>
ElseOtherwiseExpr(kind.extract, point)
}
}

View File

@ -15,24 +15,24 @@ import aqua.parser.lift.Span.{P0ToSpan, PToSpan}
case class ForExpr[F[_]](
item: Name[F],
iterable: ValueToken[F],
mode: Option[(F[ForExpr.Mode], ForExpr.Mode)]
mode: Option[ForExpr.Mode]
) extends Expr[F](ForExpr, item) {
override def mapK[K[_]: Comonad](fk: F ~> K): ForExpr[K] =
copy(item.mapK(fk), iterable.mapK(fk), mode.map { case (mF, m) => (fk(mF), m) })
copy(item.mapK(fk), iterable.mapK(fk))
}
object ForExpr extends Expr.AndIndented {
sealed trait Mode
case object TryMode extends Mode
case object ParMode extends Mode
enum Mode { case ParMode, TryMode }
override def validChildren: List[Expr.Lexem] = ArrowExpr.funcChildren
private lazy val modeP: P[Mode] =
(` ` *> (`par`.as(Mode.ParMode) | `try`.as(Mode.TryMode)).lift).map(_.extract)
override def p: P[ForExpr[Span.S]] =
((`for` *> ` ` *> Name.p <* ` <- `) ~ ValueToken.`value` ~ (` ` *> (`par`
.as(ParMode: Mode)
.lift | `try`.as(TryMode: Mode).lift)).?).map { case ((item, iterable), mode) =>
ForExpr(item, iterable, mode.map(m => m -> m.extract))
((`for` *> ` ` *> Name.p <* ` <- `) ~ ValueToken.`value` ~ modeP.?).map {
case ((item, iterable), mode) =>
ForExpr(item, iterable, mode)
}
}

View File

@ -2,7 +2,19 @@ package aqua
import aqua.AquaSpec.spanToId
import aqua.parser.expr.*
import aqua.parser.expr.func.{AbilityIdExpr, ArrowExpr, AssignmentExpr, CallArrowExpr, ClosureExpr, ElseOtherwiseExpr, ForExpr, IfExpr, OnExpr, PushToStreamExpr, ReturnExpr}
import aqua.parser.expr.func.{
AbilityIdExpr,
ArrowExpr,
AssignmentExpr,
CallArrowExpr,
ClosureExpr,
ElseOtherwiseExpr,
ForExpr,
IfExpr,
OnExpr,
PushToStreamExpr,
ReturnExpr
}
import aqua.parser.head.FromExpr.NameOrAbAs
import aqua.parser.head.{FromExpr, UseFromExpr}
import aqua.parser.lexer.*
@ -10,7 +22,7 @@ import aqua.parser.lexer.Token.LiftToken
import aqua.parser.lift.LiftParser.Implicits.idLiftParser
import aqua.types.LiteralType.{bool, number, string}
import aqua.types.{LiteralType, ScalarType}
import cats.{Id, ~>}
import cats.{~>, Id}
import org.scalatest.EitherValues
import aqua.parser.lift.Span
import aqua.parser.lift.Span.{P0ToSpan, PToSpan}
@ -128,7 +140,7 @@ trait AquaSpec extends EitherValues {
def parseFor(str: String): ForExpr[Id] =
ForExpr.p.parseAll(str).value.mapK(spanToId)
def parseElse(str: String): ElseOtherwiseExpr[Id] =
def parseElseOtherwise(str: String): ElseOtherwiseExpr[Id] =
ElseOtherwiseExpr.p.parseAll(str).value.mapK(spanToId)
def parseFieldType(str: String): FieldTypeExpr[Id] =

View File

@ -13,12 +13,14 @@ class ElseOtherwiseExprSpec extends AnyFlatSpec with Matchers with AquaSpec {
import AquaSpec._
"else" should "be parsed" in {
parseElse("else") should be(
ElseOtherwiseExpr[Id](Token.lift[Id, Unit](()))
parseElseOtherwise("else") should be(
ElseOtherwiseExpr[Id](ElseOtherwiseExpr.Kind.Else, Token.lift(()))
)
}
parseElse("otherwise") should be(
ElseOtherwiseExpr[Id](Token.lift[Id, Unit](()))
"otherwise" should "be parsed" in {
parseElseOtherwise("otherwise") should be(
ElseOtherwiseExpr[Id](ElseOtherwiseExpr.Kind.Otherwise, Token.lift(()))
)
}
}

View File

@ -27,11 +27,11 @@ class ForExprSpec extends AnyFlatSpec with Matchers with AquaSpec {
)
parseFor("for some <- false par") should be(
ForExpr[Id]("some", toBool(false), Some(ForExpr.ParMode -> ForExpr.ParMode))
ForExpr[Id]("some", toBool(false), Some(ForExpr.Mode.ParMode))
)
parseFor("for some <- false try") should be(
ForExpr[Id]("some", toBool(false), Some(ForExpr.TryMode -> ForExpr.TryMode))
ForExpr[Id]("some", toBool(false), Some(ForExpr.Mode.TryMode))
)
}

View File

@ -16,10 +16,15 @@ import aqua.semantics.rules.definitions.{
import aqua.semantics.rules.locations.{DummyLocationsInterpreter, LocationsAlgebra, LocationsState}
import aqua.semantics.rules.names.{NamesAlgebra, NamesInterpreter, NamesState}
import aqua.semantics.rules.types.{TypesAlgebra, TypesInterpreter, TypesState}
import aqua.semantics.rules.{ReportError, ValuesAlgebra}
import aqua.semantics.rules.ValuesAlgebra
import aqua.semantics.rules.errors.ReportErrors
import aqua.semantics.rules.errors.ErrorsAlgebra
import aqua.raw.ops.*
import cats.arrow.FunctionK
import cats.data.*
import cats.Reducible
import cats.data.Chain.*
import cats.data.Validated.{Invalid, Valid}
import cats.kernel.Monoid
import cats.syntax.applicative.*
@ -29,6 +34,7 @@ import cats.syntax.flatMap.*
import cats.syntax.functor.*
import cats.syntax.foldable.*
import cats.syntax.reducible.*
import cats.syntax.traverse.*
import cats.free.CofreeInstances
import cats.syntax.semigroup.*
import cats.{Eval, Monad, Semigroup}
@ -55,7 +61,7 @@ class RawSemantics[S[_]](implicit p: Picker[RawContext]) extends Semantics[S, Ra
implicit val locationsInterpreter: DummyLocationsInterpreter[S, CompilerState[S]] =
new DummyLocationsInterpreter[S, CompilerState[S]]()
Semantics
RawSemantics
.interpret(ast, CompilerState.init(init), init)
.map { case (state, ctx) =>
NonEmptyChain
@ -67,34 +73,236 @@ class RawSemantics[S[_]](implicit p: Picker[RawContext]) extends Semantics[S, Ra
}
}
object Semantics extends Logging {
object RawSemantics extends Logging {
/**
* [[RawTag.Tree]] with [[Token]] used for error reporting
*/
private final case class RawTagWithToken[S[_]](
tree: RawTag.Tree,
token: Token[S]
) {
lazy val tag: RawTag = tree.head
private def modifyTree(f: RawTag.Tree => RawTag.Tree): RawTagWithToken[S] =
copy(tree = f(tree))
/**
* Wrap tail of @param next in [[SeqTag]]
* and append it to current tree tail
*/
def append(next: RawTagWithToken[S]): RawTagWithToken[S] = modifyTree(tree =>
tree.copy(
tail = (
tree.tail,
// SeqTag.wrap will return single node as is
next.tree.tail.map(SeqTag.wrap)
).mapN(_ :+ _)
)
)
def wrapIn(tag: GroupTag): RawTagWithToken[S] = modifyTree(tree => tag.wrap(tree))
def toRaw: RawWithToken[S] = RawWithToken(FuncOp(tree), token)
}
private def elseWithoutIf[S[_], G[_]](
token: Token[S]
)(using E: ErrorsAlgebra[S, G]): G[Unit] =
E.report(token, "Unexpected `else` without `if`" :: Nil)
private def catchWithoutTry[S[_], G[_]](
token: Token[S]
)(using E: ErrorsAlgebra[S, G]): G[Unit] =
E.report(token, "Unexpected `catch` without `try`" :: Nil)
private def otherwiseWithoutPrev[S[_], G[_]](
token: Token[S]
)(using E: ErrorsAlgebra[S, G]): G[Unit] =
E.report(token, "Unexpected `otherwise` without previous instruction" :: Nil)
private def parWithoutPrev[S[_], G[_]](
token: Token[S]
)(using E: ErrorsAlgebra[S, G]): G[Unit] =
E.report(token, "Unexpected `par` without previous instruction" :: Nil)
/**
* Optionally combine two [[RawTag.Tree]] into one.
* Used to combine `if` and `else`,
* `try` and `catch` (`otherwise`);
* to create [[ParTag]] from `par`,
* [[TryTag]] from `otherwise`
*
* @param prev Previous tag
* @param next Next tag
* @param E Algebra for error reporting
* @return [[Some]] with result of combination
* [[None]] if tags should not be combined
* or error occuried
*/
private def rawTagCombine[S[_], G[_]: Monad](
prev: RawTagWithToken[S],
next: RawTagWithToken[S]
)(using E: ErrorsAlgebra[S, G]): G[Option[RawTagWithToken[S]]] =
(prev.tag, next.tag) match {
case (_: IfTag, IfTag.Else) =>
prev.append(next).some.pure
case (_, IfTag.Else) | (IfTag.Else, _) =>
val token = prev.tag match {
case IfTag.Else => prev.token
case _ => next.token
}
elseWithoutIf(token).as(none)
case (TryTag, TryTag.Catch) =>
prev.append(next).some.pure
case (_, TryTag.Catch) | (TryTag.Catch, _) =>
val token = prev.tag match {
case TryTag.Catch => prev.token
case _ => next.token
}
catchWithoutTry(token).as(none)
case (TryTag.Otherwise, _) =>
otherwiseWithoutPrev(prev.token).as(none)
case (TryTag, TryTag.Otherwise) =>
prev.append(next).some.pure
case (_, TryTag.Otherwise) =>
prev
.wrapIn(TryTag)
.append(next)
.some
.pure
case (ParTag.Par, _) =>
parWithoutPrev(prev.token).as(none)
case (ParTag, ParTag.Par) =>
prev.append(next).some.pure
case (_, ParTag.Par) =>
prev
.wrapIn(ParTag)
.append(next)
.some
.pure
case _ => none.pure
}
/**
* Check if tag is valid to be single
*
* @param single tag
* @param E Algebra for error reporting
* @return [[Some]] if tag is valid to be single
* [[None]] otherwise
*/
private def rawTagSingleCheck[S[_], G[_]: Monad](
single: RawTagWithToken[S]
)(using E: ErrorsAlgebra[S, G]): G[Option[RawTagWithToken[S]]] =
single.tag match {
case IfTag.Else => elseWithoutIf(single.token).as(none)
case TryTag.Catch => catchWithoutTry(single.token).as(none)
case TryTag.Otherwise => otherwiseWithoutPrev(single.token).as(none)
case ParTag.Par => parWithoutPrev(single.token).as(none)
case _ => single.some.pure
}
/**
* [[Raw]] with [[Token]] used for error reporting
*/
private final case class RawWithToken[S[_]](
raw: Raw,
token: Token[S]
) {
def toTag: Option[RawTagWithToken[S]] =
raw match {
case FuncOp(tree) => RawTagWithToken(tree, token).some
case _ => none
}
}
/**
* State for folding [[Raw]] results of children
*
* @param last Last seen [[Raw]] with [[Token]]
* @param acc All previous [[Raw]]
*/
private final case class InnersFoldState[S[_]](
last: Option[RawWithToken[S]] = None,
acc: Chain[Raw] = Chain.empty
) {
/**
* Process new incoming [[Raw]]
*/
def step[G[_]: Monad](
next: RawWithToken[S]
)(using ErrorsAlgebra[S, G]): G[InnersFoldState[S]] =
last.fold(copy(last = next.some).pure)(prev =>
(prev.toTag, next.toTag)
.traverseN(rawTagCombine)
.map(
_.flatten.fold(
// No combination - just update last and acc
copy(
last = next.some,
acc = prev.raw +: acc
)
)(combined =>
// Result of combination is the new last
copy(
last = combined.toRaw.some
)
)
)
)
/**
* Produce result of folding
*/
def result[G[_]: Monad](using
ErrorsAlgebra[S, G]
): G[Option[Raw]] =
if (acc.isEmpty)
// Hack to report error if single tag in block is incorrect
last.flatTraverse(single =>
single.toTag.fold(single.raw.some.pure)(singleTag =>
for {
checked <- rawTagSingleCheck(singleTag)
maybeRaw = checked.map(_.toRaw.raw)
} yield maybeRaw
)
)
else
last
.fold(acc)(_.raw +: acc)
.reverse
.reduceLeftOption(_ |+| _)
.pure
}
private def folder[S[_], G[_]: Monad](implicit
A: AbilitiesAlgebra[S, G],
N: NamesAlgebra[S, G],
T: TypesAlgebra[S, G],
D: DefinitionsAlgebra[S, G],
L: LocationsAlgebra[S, G]
): (Expr[S], Chain[G[Raw]]) => Eval[G[Raw]] = { case (expr, inners) =>
L: LocationsAlgebra[S, G],
E: ErrorsAlgebra[S, G]
): (Expr[S], Chain[G[RawWithToken[S]]]) => Eval[G[RawWithToken[S]]] = (expr, inners) =>
Eval later ExprSem
.getProg[S, G](expr)
.apply(
// TODO instead of foldRight, do slidingWindow for 2 elements, merge right associative ones
// Then foldLeft just like now
inners
.foldRight[G[List[Raw]]](List.empty[Raw].pure[G]) { case (a, b) =>
(a, b).mapN {
case (prev: FuncOp, (next: FuncOp) :: tail) if next.isRightAssoc =>
(prev :+: next) :: tail
case (prev, acc) => prev :: acc
}
}
.map(
_.reduceLeftOption(_ |+| _)
.getOrElse(Raw.empty("AST is empty"))
)
)
}
.apply(for {
children <- inners.sequence
resultState <- children
.traverse(raw => StateT.modifyF((state: InnersFoldState[S]) => state.step(raw)))
.runS(InnersFoldState())
result <- resultState.result
} yield result.getOrElse(Raw.empty("AST is empty")))
.map(raw => RawWithToken(raw, expr.token))
type Interpreter[S[_], A] = State[CompilerState[S], A]
@ -103,9 +311,14 @@ object Semantics extends Logging {
)(implicit locations: LocationsAlgebra[S, Interpreter[S, *]]): Interpreter[S, Raw] = {
import monocle.syntax.all.*
implicit val re: ReportError[S, CompilerState[S]] =
(st: CompilerState[S], token: Token[S], hints: List[String]) =>
implicit val re: ReportErrors[S, CompilerState[S]] = new ReportErrors[S, CompilerState[S]] {
override def apply(
st: CompilerState[S],
token: Token[S],
hints: List[String]
): CompilerState[S] =
st.focus(_.errors).modify(_.append(RulesViolated(token, hints)))
}
implicit val ns: Lens[CompilerState[S], NamesState[S]] = GenLens[CompilerState[S]](_.names)
@ -125,7 +338,11 @@ object Semantics extends Logging {
new NamesInterpreter[S, CompilerState[S]]
implicit val definitionsInterpreter: DefinitionsInterpreter[S, CompilerState[S]] =
new DefinitionsInterpreter[S, CompilerState[S]]
ast.cata(folder[S, Interpreter[S, *]]).value
ast
.cata(folder[S, Interpreter[S, *]])
.value
.map(_.raw)
}
private def astToState[S[_]](ast: Ast[S])(implicit
@ -166,6 +383,7 @@ object Semantics extends Logging {
) { case (ctx, p) =>
ctx.copy(parts = ctx.parts :+ (ctx -> p))
}
case (state: CompilerState[S], m) =>
logger.error("Got unexpected " + m)
state.copy(errors = state.errors :+ WrongAST(ast)) -> RawContext.blank.copy(

View File

@ -1,6 +1,6 @@
package aqua.semantics.expr.func
import aqua.raw.ops.{AssignmentTag, FuncOp, SeqTag, XorTag}
import aqua.raw.ops.{AssignmentTag, FuncOp, SeqTag, TryTag}
import aqua.parser.expr.func.CatchExpr
import aqua.raw.value.ValueRaw
import aqua.raw.Raw
@ -22,23 +22,26 @@ class CatchSem[S[_]](val expr: CatchExpr[S]) extends AnyVal {
): Prog[Alg, Raw] =
Prog
.around(
N.beginScope(expr.name) >> L.beginScope() >>
N.beginScope(expr.name) >>
L.beginScope() >>
N.define(expr.name, ValueRaw.LastError.baseType),
(_: Boolean, g: Raw) =>
(_, g: Raw) =>
N.endScope() >> L.endScope() as (
g match {
case FuncOp(op) =>
N.endScope() >> L.endScope() as (XorTag
TryTag.Catch
.wrap(
SeqTag.wrap(
AssignmentTag(ValueRaw.LastError, expr.name.value).leaf,
op
)
)
.toFuncOp: Raw)
.toFuncOp
case _ =>
N.endScope() >> L.endScope() as g
Raw.error("Wrong body of the `catch` expression")
}
)
)
.abilitiesScope[S](expr.token)
}

View File

@ -1,12 +1,13 @@
package aqua.semantics.expr.func
import aqua.raw.ops.{FuncOp, XorTag}
import aqua.raw.ops.{FuncOp, IfTag, TryTag}
import aqua.parser.expr.func.ElseOtherwiseExpr
import aqua.raw.Raw
import aqua.semantics.Prog
import aqua.semantics.rules.abilities.AbilitiesAlgebra
import aqua.semantics.rules.locations.LocationsAlgebra
import aqua.semantics.rules.names.NamesAlgebra
import cats.syntax.applicative.*
import cats.Monad
@ -19,8 +20,18 @@ class ElseOtherwiseSem[S[_]](val expr: ElseOtherwiseExpr[S]) extends AnyVal {
): Prog[Alg, Raw] =
Prog
.after[Alg, Raw] {
case FuncOp(g) => XorTag.wrap(g).toFuncOp.pure[Alg]
case g => g.pure[Alg]
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
}
.abilitiesScope(expr.token)
.namesScope(expr.token)

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}
import cats.Monad
import cats.data.Chain
import cats.syntax.applicative.*
@ -42,27 +43,19 @@ class ForSem[S[_]](val expr: ForExpr[S]) extends AnyVal {
(stOpt: Option[ValueRaw], ops: Raw) =>
N.streamsDefinedWithinScope()
.map(_.keySet)
.map((streams: Set[String]) =>
.map(streams =>
(stOpt, ops) match {
case (Some(vm), FuncOp(op)) =>
val innerTag = expr.mode.map(_._2).fold[RawTag](SeqTag) {
case ForExpr.ParMode => ParTag
case ForExpr.TryMode => XorTag
val innerTag = expr.mode.fold[RawTag](SeqTag) {
case ForExpr.Mode.ParMode => ParTag
case ForExpr.Mode.TryMode => TryTag
}
val mode = expr.mode.map(_._2).flatMap {
case ForExpr.ParMode => Some(WaitMode)
case ForExpr.TryMode => None
}
val mode = expr.mode.collect { case ForExpr.Mode.ParMode => WaitMode }
val forTag =
ForTag(expr.item.value, vm, mode).wrap(
expr.mode
.map(_._2)
.fold[RawTag](SeqTag) {
case ForExpr.ParMode => ParTag
case ForExpr.TryMode => XorTag
}
innerTag
.wrap(
// Restrict the streams created within this scope
streams.toList.foldLeft(op) { case (b, streamName) =>
@ -76,7 +69,7 @@ class ForSem[S[_]](val expr: ForExpr[S]) extends AnyVal {
if (innerTag == ParTag) ParTag.Detach.wrap(forTag).toFuncOp
else forTag.toFuncOp
case _ =>
Raw.error("Wrong body of the For expression")
Raw.error("Wrong body of the `for` expression")
}
)
)

View File

@ -1,6 +1,6 @@
package aqua.semantics.expr.func
import aqua.raw.ops.{FuncOp, MatchMismatchTag, XorTag}
import aqua.raw.ops.{FuncOp, IfTag}
import aqua.parser.expr.func.IfExpr
import aqua.raw.value.ValueRaw
import aqua.raw.Raw
@ -11,10 +11,12 @@ import aqua.semantics.rules.locations.LocationsAlgebra
import aqua.semantics.rules.names.NamesAlgebra
import aqua.semantics.rules.types.TypesAlgebra
import aqua.types.Type
import cats.Monad
import cats.syntax.applicative.*
import cats.syntax.flatMap.*
import cats.syntax.functor.*
import cats.syntax.apply.*
class IfSem[S[_]](val expr: IfExpr[S]) extends AnyVal {
@ -27,36 +29,31 @@ class IfSem[S[_]](val expr: IfExpr[S]) extends AnyVal {
): Prog[Alg, Raw] =
Prog
.around(
V.valueToRaw(expr.left).flatMap {
case Some(lt) =>
V.valueToRaw(expr.right).flatMap {
case Some(rt) =>
T.ensureValuesComparable(expr.right, lt.`type`, rt.`type`)
.map(m => Some(lt -> rt).filter(_ => m))
case None =>
None.pure[Alg]
}
case None =>
V.resolveType(expr.right).as[Option[(ValueRaw, ValueRaw)]](None)
(V.valueToRaw(expr.left), V.valueToRaw(expr.right)).flatMapN {
case (Some(lt), Some(rt)) =>
T.ensureValuesComparable(
token = expr.token,
left = lt.`type`,
right = rt.`type`
).map(Option.when(_)(lt -> rt))
case _ => None.pure
},
(r: Option[(ValueRaw, ValueRaw)], ops: Raw) =>
r.fold(Raw.error("If expression errored in matching types").pure[Alg]) { case (lt, rt) =>
(values: Option[(ValueRaw, ValueRaw)], ops: Raw) =>
values
.fold(
Raw.error("`if` expression errored in matching types")
) { case (lt, rt) =>
ops match {
case FuncOp(op) =>
XorTag.LeftBiased
.wrap(
MatchMismatchTag(
lt,
rt,
expr.eqOp.value
).wrap(op)
)
.toFuncOp
.pure[Alg]
case _ => Raw.error("Wrong body of the if expression").pure[Alg]
IfTag(
left = lt,
right = rt,
equal = expr.eqOp.value
).wrap(op).toFuncOp
case _ => Raw.error("Wrong body of the `if` expression")
}
}
.pure
)
.abilitiesScope[S](expr.token)
.namesScope[S](expr.token)

View File

@ -12,7 +12,7 @@ class ParSem[S[_]](val expr: ParExpr[S]) extends AnyVal {
def program[Alg[_]: Monad]: Prog[Alg, Raw] =
Prog.after[Alg, Raw] {
case FuncOp(g) =>
ParTag.wrap(g).toFuncOp.pure[Alg]
ParTag.Par.wrap(g).toFuncOp.pure[Alg]
case g => g.pure[Alg]
}
}

View File

@ -1,6 +1,6 @@
package aqua.semantics.expr.func
import aqua.raw.ops.{FuncOp, XorTag}
import aqua.raw.ops.{FuncOp, TryTag}
import aqua.parser.expr.func.TryExpr
import aqua.raw.Raw
import aqua.semantics.Prog
@ -22,9 +22,9 @@ class TrySem[S[_]](val expr: TryExpr[S]) extends AnyVal {
Prog
.after[Alg, Raw] {
case FuncOp(o) =>
XorTag.LeftBiased.wrap(o).toFuncOp.pure[Alg]
TryTag.wrap(o).toFuncOp.pure[Alg]
case _ =>
Raw.error("Wrong body of the try expression").pure[Alg]
Raw.error("Wrong body of the `try` expression").pure[Alg]
}
.abilitiesScope(expr.token)
.namesScope(expr.token)

View File

@ -1,7 +0,0 @@
package aqua.semantics.rules
import aqua.parser.lexer.Token
trait ReportError[S[_], X] {
def apply(st: X, token: Token[S], hints: List[String]): X
}

View File

@ -4,10 +4,11 @@ import aqua.parser.lexer.Token
import cats.data.State
import monocle.Lens
import cats.syntax.functor.*
import aqua.semantics.rules.errors.ReportErrors
case class StackInterpreter[S[_], X, St, Fr](stackLens: Lens[St, List[Fr]])(implicit
lens: Lens[X, St],
error: ReportError[S, X]
error: ReportErrors[S, X]
) {
type SX[A] = State[X, A]

View File

@ -7,7 +7,8 @@ import aqua.raw.value.ValueRaw
import aqua.semantics.Levenshtein
import aqua.semantics.rules.definitions.DefinitionsAlgebra
import aqua.semantics.rules.locations.LocationsAlgebra
import aqua.semantics.rules.{ReportError, StackInterpreter, abilities}
import aqua.semantics.rules.{abilities, StackInterpreter}
import aqua.semantics.rules.errors.ReportErrors
import aqua.types.ArrowType
import cats.data.{NonEmptyList, NonEmptyMap, State}
import cats.syntax.functor.*
@ -17,7 +18,7 @@ import monocle.macros.GenLens
class AbilitiesInterpreter[S[_], X](implicit
lens: Lens[X, AbilitiesState[S]],
error: ReportError[S, X],
error: ReportErrors[S, X],
locations: LocationsAlgebra[S, State[X, *]]
) extends AbilitiesAlgebra[S, State[X, *]] {
@ -46,11 +47,14 @@ class AbilitiesInterpreter[S[_], X](implicit
s.copy(
services = s.services
.updated(name.value, ServiceRaw(name.value, arrows.map(_._2), defaultId)),
definitions =
s.definitions.updated(name.value, name)
definitions = s.definitions.updated(name.value, name)
)
).flatMap { _ =>
locations.addTokenWithFields(name.value, name, arrows.toNel.toList.map(t => t._1 -> t._2._1))
locations.addTokenWithFields(
name.value,
name,
arrows.toNel.toList.map(t => t._1 -> t._2._1)
)
}.as(true)
}

View File

@ -1,7 +1,8 @@
package aqua.semantics.rules.definitions
import aqua.parser.lexer.{Name, NamedTypeToken, Token}
import aqua.semantics.rules.{ReportError, StackInterpreter}
import aqua.semantics.rules.StackInterpreter
import aqua.semantics.rules.errors.ReportErrors
import aqua.semantics.rules.abilities.AbilitiesState
import aqua.semantics.rules.locations.{LocationsAlgebra, LocationsState}
import aqua.semantics.rules.types.TypesState
@ -18,7 +19,7 @@ import scala.collection.immutable.SortedMap
class DefinitionsInterpreter[S[_], X](implicit
lens: Lens[X, DefinitionsState[S]],
error: ReportError[S, X],
error: ReportErrors[S, X],
locations: LocationsAlgebra[S, State[X, *]]
) extends DefinitionsAlgebra[S, State[X, *]] {
type SX[A] = State[X, A]

View File

@ -0,0 +1,7 @@
package aqua.semantics.rules.errors
import aqua.parser.lexer.Token
trait ErrorsAlgebra[S[_], Alg[_]] {
def report(token: Token[S], hints: List[String]): Alg[Unit]
}

View File

@ -0,0 +1,12 @@
package aqua.semantics.rules.errors
import aqua.parser.lexer.Token
import cats.data.State
trait ReportErrors[S[_], X] extends ErrorsAlgebra[S, State[X, *]] {
def apply(st: X, token: Token[S], hints: List[String]): X
def report(token: Token[S], hints: List[String]): State[X, Unit] =
State.modify(apply(_, token, hints))
}

View File

@ -3,7 +3,8 @@ package aqua.semantics.rules.names
import aqua.parser.lexer.{Name, Token}
import aqua.semantics.Levenshtein
import aqua.semantics.rules.locations.LocationsAlgebra
import aqua.semantics.rules.{ReportError, StackInterpreter}
import aqua.semantics.rules.StackInterpreter
import aqua.semantics.rules.errors.ReportErrors
import aqua.types.{ArrowType, StreamType, Type}
import cats.data.{OptionT, State}
import cats.syntax.flatMap.*
@ -14,7 +15,7 @@ import monocle.macros.GenLens
class NamesInterpreter[S[_], X](implicit
lens: Lens[X, NamesState[S]],
error: ReportError[S, X],
error: ReportErrors[S, X],
locations: LocationsAlgebra[S, State[X, *]]
) extends NamesAlgebra[S, State[X, *]] {

View File

@ -3,7 +3,8 @@ package aqua.semantics.rules.types
import aqua.parser.lexer.*
import aqua.raw.value.{FunctorRaw, IntoCopyRaw, IntoFieldRaw, IntoIndexRaw, PropertyRaw, ValueRaw}
import aqua.semantics.rules.locations.LocationsAlgebra
import aqua.semantics.rules.{ReportError, StackInterpreter}
import aqua.semantics.rules.StackInterpreter
import aqua.semantics.rules.errors.ReportErrors
import aqua.types.{
ArrayType,
ArrowType,
@ -32,7 +33,7 @@ import scala.collection.immutable.SortedMap
class TypesInterpreter[S[_], X](implicit
lens: Lens[X, TypesState[S]],
error: ReportError[S, X],
error: ReportErrors[S, X],
locations: LocationsAlgebra[S, State[X, *]]
) extends TypesAlgebra[S, State[X, *]] {

View File

@ -6,7 +6,6 @@ import aqua.raw.arrow.ArrowRaw
import aqua.raw.Raw
import aqua.raw.ops.{EmptyTag, FuncOp, RawTag}
import aqua.semantics.expr.func.ClosureSem
import aqua.semantics.rules.ReportError
import aqua.semantics.rules.names.{NamesInterpreter, NamesState}
import aqua.types.{ArrowType, ProductType}
import cats.data.*

View File

@ -6,22 +6,68 @@ import aqua.raw.ops.{Call, CallArrowRawTag, FuncOp, OnTag, ParTag, RawTag, SeqGr
import aqua.parser.Parser
import aqua.parser.lift.{LiftParser, Span}
import aqua.raw.value.{LiteralRaw, ValueRaw}
import aqua.types.{ArrowType, ConsType, LiteralType, NilType, ScalarType}
import aqua.types.*
import aqua.raw.ops.*
import org.scalatest.flatspec.AnyFlatSpec
import org.scalatest.matchers.should.Matchers
import org.scalatest.Inside
import cats.~>
import cats.data.Chain
import cats.data.NonEmptyChain
import cats.syntax.show.*
import cats.data.Validated
class SemanticsSpec extends AnyFlatSpec with Matchers {
class SemanticsSpec extends AnyFlatSpec with Matchers with Inside {
val emptyCall = Call(Nil, Nil)
// use it to fix https://github.com/fluencelabs/aqua/issues/90
"sem" should "create right model" in {
implicit val fileLift: LiftParser[Span.S] = Span.spanLiftParser
val parser = Parser.parse(Parser.spanParser)
val semantics = new RawSemantics[Span.S]()
def insideBody(script: String)(test: RawTag.Tree => Any): Unit =
inside(parser(script)) { case Validated.Valid(ast) =>
val init = RawContext.blank
inside(semantics.process(ast, init)) { case Validated.Valid(ctx) =>
inside(ctx.funcs.headOption) { case Some((_, func)) =>
test(func.arrow.body)
}
}
}
def insideSemErrors(script: String)(test: NonEmptyChain[SemanticError[Span.S]] => Any): Unit =
inside(parser(script)) { case Validated.Valid(ast) =>
val init = RawContext.blank
inside(semantics.process(ast, init)) { case Validated.Invalid(errors) =>
test(errors)
}
}
val testServiceDef = """
|service Test("test"):
| testCall()
| testCallStr(s: string) -> string
|
""".stripMargin
def testServiceCallStr(str: String) =
CallArrowRawTag
.service(
serviceId = LiteralRaw.quote("test"),
fnName = "testCallStr",
call = Call(LiteralRaw.quote(str) :: Nil, Nil),
name = "Test",
arrowType = ArrowType(
ProductType.labelled(("s" -> ScalarType.string) :: Nil),
ProductType(ScalarType.string :: Nil)
)
)
.leaf
// use it to fix https://github.com/fluencelabs/aqua/issues/90
"semantics" should "create right model" in {
val script =
"""service A("srv1"):
| fn1: -> string
@ -31,24 +77,12 @@ class SemanticsSpec extends AnyFlatSpec with Matchers {
| A.fn1()
| par A.fn1()""".stripMargin
val ast = parser(script).toList.head
val ctx = RawContext.blank
val semantics = new RawSemantics[Span.S]()
val p = semantics.process(ast, ctx)
val func = p.toList.head.funcs("parFunc")
val proc = func.arrow.body
insideBody(script) { body =>
val arrowType = ArrowType(NilType, ConsType.cons(ScalarType.string, NilType))
val serviceCall =
CallArrowRawTag.service(LiteralRaw.quote("srv1"), "fn1", emptyCall, "A", arrowType).leaf
val expected =
SeqGroupTag.wrap(
ParTag.wrap(
OnTag(
LiteralRaw("\"other-peer\"", LiteralType.string),
@ -58,9 +92,358 @@ class SemanticsSpec extends AnyFlatSpec with Matchers {
),
serviceCall
)
body.equalsOrShowDiff(expected) should be(true)
}
}
it should "handle if with else" in {
val script = testServiceDef +
"""
|func test():
| if 1 == 2:
| Test.testCallStr("if")
| else:
| Test.testCallStr("else")
|""".stripMargin
insideBody(script) { body =>
val expected =
IfTag(LiteralRaw.number(1), LiteralRaw.number(2), true).wrap(
testServiceCallStr("if"),
testServiceCallStr("else")
)
proc.equalsOrShowDiff(expected) should be(true)
body.equalsOrShowDiff(expected) should be(true)
}
}
it should "handle try with catch" in {
val script = testServiceDef +
"""
|func test():
| try:
| Test.testCallStr("try")
| catch e:
| Test.testCallStr("catch")
|""".stripMargin
insideBody(script) { body =>
val expected =
TryTag.wrap(
testServiceCallStr("try"),
SeqTag.wrap(
AssignmentTag(ValueRaw.LastError, "e").leaf,
testServiceCallStr("catch")
)
)
body.equalsOrShowDiff(expected) should be(true)
}
}
it should "handle try with more than one catch" in {
val script = testServiceDef +
"""
|func test():
| try:
| Test.testCallStr("try")
| catch e:
| Test.testCallStr("catch1")
| catch e:
| Test.testCallStr("catch2")
|""".stripMargin
insideBody(script) { body =>
val expected =
TryTag.wrap(
testServiceCallStr("try"),
SeqTag.wrap(
AssignmentTag(ValueRaw.LastError, "e").leaf,
testServiceCallStr("catch1")
),
SeqTag.wrap(
AssignmentTag(ValueRaw.LastError, "e").leaf,
testServiceCallStr("catch2")
)
)
body.equalsOrShowDiff(expected) should be(true)
}
}
it should "handle try with otherwise" in {
val script = testServiceDef +
"""
|func test():
| try:
| Test.testCallStr("try")
| otherwise:
| Test.testCallStr("otherwise")
|""".stripMargin
insideBody(script) { body =>
val expected =
TryTag.wrap(
testServiceCallStr("try"),
testServiceCallStr("otherwise")
)
body.equalsOrShowDiff(expected) should be(true)
}
}
it should "handle if without else" in {
val script = testServiceDef +
"""
|func test():
| if 1 != 2:
| Test.testCallStr("if")
|""".stripMargin
insideBody(script) { body =>
val expected =
IfTag(LiteralRaw.number(1), LiteralRaw.number(2), false).wrap(
testServiceCallStr("if")
)
body.equalsOrShowDiff(expected) should be(true)
}
}
it should "handle try without catch" in {
val script = testServiceDef +
"""
|func test():
| try:
| Test.testCallStr("try")
|""".stripMargin
insideBody(script) { body =>
val expected =
TryTag.wrap(
testServiceCallStr("try")
)
body.equalsOrShowDiff(expected) should be(true)
}
}
it should "handle par" in {
val tests = List("two", "three", "four", "five")
(1 to tests.length)
.map(tests.take(_))
.foreach(test =>
val script = testServiceDef +
"""
|func test():
| Test.testCallStr("one")
|""".stripMargin +
test.map(n => s" par Test.testCallStr(\"$n\")\n").mkString
insideBody(script) { body =>
val expected = ParTag.wrap(
testServiceCallStr("one") +: test.map(n => testServiceCallStr(n))
)
body.equalsOrShowDiff(expected) should be(true)
}
)
}
it should "handle otherwise" in {
val script = testServiceDef +
"""
|func test():
| Test.testCallStr("fail")
| otherwise:
| Test.testCallStr("otherwise")
|""".stripMargin
insideBody(script) { body =>
val expected = TryTag.wrap(
testServiceCallStr("fail"),
testServiceCallStr("otherwise")
)
body.equalsOrShowDiff(expected) should be(true)
}
}
it should "handle if with otherwise" in {
val script = testServiceDef +
"""
|func test():
| if "a" != "b":
| Test.testCallStr("if")
| otherwise:
| Test.testCallStr("otherwise")
|""".stripMargin
insideBody(script) { body =>
val expected = TryTag.wrap(
IfTag(LiteralRaw.quote("a"), LiteralRaw.quote("b"), false).wrap(
testServiceCallStr("if")
),
testServiceCallStr("otherwise")
)
body.equalsOrShowDiff(expected) should be(true)
}
}
it should "handle if and try with par" in {
val tests = List("two", "three", "four", "five")
val ifTry = List(
"""
| if "a" != "b":
| Test.testCallStr("if")
|""".stripMargin ->
IfTag(LiteralRaw.quote("a"), LiteralRaw.quote("b"), false).wrap(
testServiceCallStr("if")
),
"""
| try:
| Test.testCallStr("try")
|""".stripMargin ->
TryTag.wrap(
testServiceCallStr("try")
)
)
(1 to tests.length)
.map(tests.take(_))
.flatMap(test => ifTry.map(test -> _))
.foreach { case (test, (ifOrTry, tag)) =>
val script = testServiceDef +
"""
|func test():
""".stripMargin +
ifOrTry +
test.map(n => s" par Test.testCallStr(\"$n\")\n").mkString
insideBody(script) { body =>
val expected = ParTag.wrap(
tag +: test.map(n => testServiceCallStr(n))
)
body.equalsOrShowDiff(expected) should be(true)
}
}
}
it should "forbid else without if" in {
val scriptTry = testServiceDef +
"""
|func test():
| try:
| Test.testCallStr("try")
| else:
| Test.testCallStr("else")
|""".stripMargin
val scriptSingle = testServiceDef +
"""
|func test():
| else:
| Test.testCallStr("else")
|""".stripMargin
insideSemErrors(scriptTry) { errors =>
atLeast(1, errors.toChain.toList) shouldBe a[RulesViolated[Span.S]]
}
insideSemErrors(scriptSingle) { errors =>
atLeast(1, errors.toChain.toList) shouldBe a[RulesViolated[Span.S]]
}
}
it should "forbid catch without try" in {
val scriptIf = testServiceDef +
"""
|func test():
| if 1 != 2:
| Test.testCallStr("if")
| catch e:
| Test.testCallStr("catch")
|""".stripMargin
val scriptSingle = testServiceDef +
"""
|func test():
| catch e:
| Test.testCallStr("catch")
|""".stripMargin
insideSemErrors(scriptIf) { errors =>
atLeast(1, errors.toChain.toList) shouldBe a[RulesViolated[Span.S]]
}
insideSemErrors(scriptSingle) { errors =>
atLeast(1, errors.toChain.toList) shouldBe a[RulesViolated[Span.S]]
}
}
it should "forbid otherwise without previous instruction" in {
val scriptOtherwise = testServiceDef +
"""
|func test():
| otherwise:
| Test.testCallStr("otherwise")
|""".stripMargin
insideSemErrors(scriptOtherwise) { errors =>
atLeast(1, errors.toChain.toList) shouldBe a[RulesViolated[Span.S]]
}
}
it should "forbid par without previous instruction" in {
val scriptOtherwise = testServiceDef +
"""
|func test():
| par Test.testCallStr("par")
|""".stripMargin
insideSemErrors(scriptOtherwise) { errors =>
atLeast(1, errors.toChain.toList) shouldBe a[RulesViolated[Span.S]]
}
}
it should "handle complex cases" in {
val script = testServiceDef +
"""
|func test():
| if "a" != "b":
| Test.testCallStr("if")
| otherwise:
| Test.testCallStr("otherwise1")
| par Test.testCallStr("par1")
| otherwise:
| Test.testCallStr("otherwise2")
| par Test.testCallStr("par2")
| par Test.testCallStr("par3")
|""".stripMargin
insideBody(script) { body =>
val expected = ParTag.wrap(
TryTag.wrap(
ParTag.wrap(
TryTag.wrap(
IfTag(LiteralRaw.quote("a"), LiteralRaw.quote("b"), false).wrap(
testServiceCallStr("if")
),
testServiceCallStr("otherwise1")
),
testServiceCallStr("par1")
),
testServiceCallStr("otherwise2")
),
testServiceCallStr("par2"),
testServiceCallStr("par3")
)
body.equalsOrShowDiff(expected) should be(true)
}
}
}

View File

@ -5,23 +5,29 @@ import aqua.parser.lexer.{Name, Token}
import aqua.parser.lift.Span
import aqua.raw.{Raw, RawContext}
import aqua.semantics.expr.func.ClosureSem
import aqua.semantics.rules.ReportError
import aqua.semantics.rules.errors.ReportErrors
import aqua.semantics.rules.abilities.{AbilitiesInterpreter, AbilitiesState}
import aqua.semantics.rules.locations.DummyLocationsInterpreter
import aqua.semantics.rules.names.{NamesInterpreter, NamesState}
import aqua.semantics.rules.types.{TypesInterpreter, TypesState}
import aqua.types.*
import cats.data.State
import cats.{Id, ~>}
import cats.{~>, Id}
import monocle.Lens
import monocle.macros.GenLens
import monocle.syntax.all.*
object Utils {
implicit val re: ReportError[Id, CompilerState[Id]] =
(st: CompilerState[Id], token: Token[Id], hints: List[String]) =>
implicit val re: ReportErrors[Id, CompilerState[Id]] = new ReportErrors[Id, CompilerState[Id]] {
override def apply(
st: CompilerState[Id],
token: Token[Id],
hints: List[String]
): CompilerState[Id] =
st.focus(_.errors).modify(_.append(RulesViolated(token, hints)))
}
implicit val locationsInterpreter: DummyLocationsInterpreter[Id, CompilerState[Id]] =
new DummyLocationsInterpreter[Id, CompilerState[Id]]()