From 3581d79869da122bb529bfcf1bb103424b5e70d4 Mon Sep 17 00:00:00 2001 From: InversionSpaces Date: Fri, 14 Jul 2023 13:43:00 +0000 Subject: [PATCH] Add tests --- .../scala/aqua/parser/lexer/ValueToken.scala | 8 +- .../scala/aqua/semantics/CompilerState.scala | 28 +++ .../main/scala/aqua/semantics/Semantics.scala | 20 -- .../aqua/semantics/rules/ValuesAlgebra.scala | 25 ++- .../aqua/semantics/ValuesAlgebraSpec.scala | 206 ++++++++++++++++++ .../main/scala/aqua/types/CompareTypes.scala | 3 +- types/src/main/scala/aqua/types/Type.scala | 13 +- 7 files changed, 272 insertions(+), 31 deletions(-) create mode 100644 semantics/src/test/scala/aqua/semantics/ValuesAlgebraSpec.scala diff --git a/parser/src/main/scala/aqua/parser/lexer/ValueToken.scala b/parser/src/main/scala/aqua/parser/lexer/ValueToken.scala index 25733e68f..105597f6b 100644 --- a/parser/src/main/scala/aqua/parser/lexer/ValueToken.scala +++ b/parser/src/main/scala/aqua/parser/lexer/ValueToken.scala @@ -96,7 +96,8 @@ object CallArrowToken { Name.p ~ abilities().? ~ comma0(ValueToken.`value`.surroundedBy(`/s*`)) .between(` `.?.with1 *> `(` <* `/s*`, `/s*` *> `)`) - ).map { case ((n, ab), args) => + ) + .map { case ((n, ab), args) => CallBraces(n, ab.map(_.toList).getOrElse(Nil), args) } .withContext( @@ -171,6 +172,11 @@ object InfixToken { def p: P[Unit] = P.string(symbol) + object Op { + val math: List[Op] = List(Pow, Mul, Div, Rem, Add, Sub) + val compare: List[Op] = List(Gt, Gte, Lt, Lte) + } + private def opsParser(ops: List[Op]): P[(Span, Op)] = P.oneOf(ops.map(op => op.p.lift.map(s => s.as(op)))) diff --git a/semantics/src/main/scala/aqua/semantics/CompilerState.scala b/semantics/src/main/scala/aqua/semantics/CompilerState.scala index 3a98e1da3..366f76b7e 100644 --- a/semantics/src/main/scala/aqua/semantics/CompilerState.scala +++ b/semantics/src/main/scala/aqua/semantics/CompilerState.scala @@ -8,10 +8,14 @@ import aqua.semantics.rules.definitions.DefinitionsState import aqua.semantics.rules.locations.LocationsState import aqua.semantics.rules.names.NamesState import aqua.semantics.rules.types.TypesState +import aqua.semantics.rules.errors.ReportErrors + import cats.Semigroup import cats.data.{Chain, State} import cats.kernel.Monoid import cats.syntax.monoid.* +import monocle.Lens +import monocle.macros.GenLens case class CompilerState[S[_]]( errors: Chain[SemanticError[S]] = Chain.empty[SemanticError[S]], @@ -32,6 +36,30 @@ object CompilerState { types = TypesState.init[F](ctx) ) + given [S[_]]: Lens[CompilerState[S], NamesState[S]] = + GenLens[CompilerState[S]](_.names) + + given [S[_]]: Lens[CompilerState[S], AbilitiesState[S]] = + GenLens[CompilerState[S]](_.abilities) + + given [S[_]]: Lens[CompilerState[S], TypesState[S]] = + GenLens[CompilerState[S]](_.types) + + given [S[_]]: Lens[CompilerState[S], DefinitionsState[S]] = + GenLens[CompilerState[S]](_.definitions) + + given [S[_]]: ReportErrors[S, CompilerState[S]] = + new ReportErrors[S, CompilerState[S]] { + import monocle.syntax.all.* + + override def apply( + st: CompilerState[S], + token: Token[S], + hints: List[String] + ): CompilerState[S] = + st.focus(_.errors).modify(_.append(RulesViolated(token, hints))) + } + implicit def compilerStateMonoid[S[_]]: Monoid[St[S]] = new Monoid[St[S]] { override def empty: St[S] = State.pure(Raw.Empty("compiler state monoid empty")) diff --git a/semantics/src/main/scala/aqua/semantics/Semantics.scala b/semantics/src/main/scala/aqua/semantics/Semantics.scala index 0d6549f41..27ad39e05 100644 --- a/semantics/src/main/scala/aqua/semantics/Semantics.scala +++ b/semantics/src/main/scala/aqua/semantics/Semantics.scala @@ -309,26 +309,6 @@ object RawSemantics extends Logging { def transpile[S[_]]( ast: Ast[S] )(implicit locations: LocationsAlgebra[S, Interpreter[S, *]]): Interpreter[S, Raw] = { - import monocle.syntax.all.* - - 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) - - implicit val as: Lens[CompilerState[S], AbilitiesState[S]] = - GenLens[CompilerState[S]](_.abilities) - - implicit val ts: Lens[CompilerState[S], TypesState[S]] = GenLens[CompilerState[S]](_.types) - - implicit val ds: Lens[CompilerState[S], DefinitionsState[S]] = - GenLens[CompilerState[S]](_.definitions) implicit val typesInterpreter: TypesInterpreter[S, CompilerState[S]] = new TypesInterpreter[S, CompilerState[S]] diff --git a/semantics/src/main/scala/aqua/semantics/rules/ValuesAlgebra.scala b/semantics/src/main/scala/aqua/semantics/rules/ValuesAlgebra.scala index 6d8620e9b..d5ddca72c 100644 --- a/semantics/src/main/scala/aqua/semantics/rules/ValuesAlgebra.scala +++ b/semantics/src/main/scala/aqua/semantics/rules/ValuesAlgebra.scala @@ -17,6 +17,7 @@ import cats.syntax.traverse.* import cats.syntax.option.* import cats.instances.list.* import cats.data.{NonEmptyList, NonEmptyMap} +import scribe.Logging import scala.collection.immutable.SortedMap @@ -24,7 +25,7 @@ class ValuesAlgebra[S[_], Alg[_]: Monad](implicit N: NamesAlgebra[S, Alg], T: TypesAlgebra[S, Alg], A: AbilitiesAlgebra[S, Alg] -) { +) extends Logging { def ensureIsString(v: ValueToken[S]): Alg[Boolean] = ensureTypeMatches(v, LiteralType.string) @@ -198,16 +199,32 @@ class ValuesAlgebra[S[_], Alg[_]: Monad](implicit case Op.Lte => ("cmp", "lte") } + /* + * If `uType == TopType`, it means that we don't + * have type big enough to hold the result of operation. + * e.g. We will use `i64` for result of `i32 * u64` + * TODO: Handle this more gracefully + * (use warning system when it is implemented) + */ + def uTypeBounded = if (uType == TopType) { + val bounded = ScalarType.i64 + logger.warn( + s"Result type of ($lType ${it.op} $rType) is $TopType, " + + s"using $bounded instead" + ) + bounded + } else uType + // Expected type sets of left and right operands, result type val (leftExp, rightExp, resType) = it.op match { case Op.Add | Op.Sub | Op.Div | Op.Rem => - (ScalarType.integer, ScalarType.integer, uType) + (ScalarType.integer, ScalarType.integer, uTypeBounded) case Op.Pow => - (ScalarType.integer, ScalarType.unsigned, uType) + (ScalarType.integer, ScalarType.unsigned, uTypeBounded) case Op.Mul if hasFloat => (ScalarType.float, ScalarType.float, ScalarType.i64) case Op.Mul => - (ScalarType.integer, ScalarType.integer, uType) + (ScalarType.integer, ScalarType.integer, uTypeBounded) case Op.Gt | Op.Lt | Op.Gte | Op.Lte => (ScalarType.integer, ScalarType.integer, ScalarType.bool) } diff --git a/semantics/src/test/scala/aqua/semantics/ValuesAlgebraSpec.scala b/semantics/src/test/scala/aqua/semantics/ValuesAlgebraSpec.scala new file mode 100644 index 000000000..b01814503 --- /dev/null +++ b/semantics/src/test/scala/aqua/semantics/ValuesAlgebraSpec.scala @@ -0,0 +1,206 @@ +package aqua.semantics + +import aqua.semantics.rules.ValuesAlgebra +import aqua.semantics.rules.names.NamesState +import aqua.semantics.rules.abilities.AbilitiesState +import aqua.semantics.rules.types.TypesState +import aqua.semantics.rules.types.TypesAlgebra +import aqua.semantics.rules.abilities.AbilitiesInterpreter +import aqua.semantics.rules.names.NamesAlgebra +import aqua.semantics.rules.definitions.DefinitionsAlgebra +import aqua.semantics.rules.abilities.AbilitiesAlgebra +import aqua.semantics.rules.names.NamesInterpreter +import aqua.semantics.rules.definitions.DefinitionsInterpreter +import aqua.semantics.rules.types.TypesInterpreter +import aqua.semantics.rules.locations.LocationsAlgebra +import aqua.semantics.rules.locations.DummyLocationsInterpreter +import aqua.raw.value.LiteralRaw +import aqua.raw.RawContext +import aqua.types.{LiteralType, ScalarType, TopType, Type} +import aqua.parser.lexer.{InfixToken, LiteralToken, Name, ValueToken, VarToken} + +import org.scalatest.flatspec.AnyFlatSpec +import org.scalatest.matchers.should.Matchers +import org.scalatest.Inside +import cats.Id +import cats.data.State +import cats.syntax.functor.* +import cats.syntax.comonad.* +import monocle.syntax.all.* + +class ValuesAlgebraSpec extends AnyFlatSpec with Matchers with Inside { + + type TestState = CompilerState[Id] + + def algebra() = { + type Interpreter[A] = State[TestState, A] + + given LocationsAlgebra[Id, Interpreter] = + new DummyLocationsInterpreter[Id, CompilerState[Id]] + + given TypesAlgebra[Id, Interpreter] = + new TypesInterpreter[Id, CompilerState[Id]] + given AbilitiesAlgebra[Id, Interpreter] = + new AbilitiesInterpreter[Id, CompilerState[Id]] + given NamesAlgebra[Id, Interpreter] = + new NamesInterpreter[Id, CompilerState[Id]] + given DefinitionsAlgebra[Id, Interpreter] = + new DefinitionsInterpreter[Id, CompilerState[Id]] + + new ValuesAlgebra[Id, Interpreter] + } + + def literal(value: String, `type`: LiteralType) = + LiteralToken(Id(value), `type`) + + def variable(name: String) = + VarToken(Name(Id(name)), Nil) + + def allPairs[A](list: List[A]): List[(A, A)] = for { + a <- list + b <- list + } yield (a, b) + + def genState(vars: Map[String, Type] = Map.empty) = + CompilerState + .init[Id](RawContext.blank) + .focus(_.names) + .modify( + _.focus(_.stack).modify( + NamesState.Frame( + token = Name(Id("test")), // Token just for test + names = vars + ) :: _ + ) + ) + + "valueToRaw" should "handle +, -, /, *, % on number literals" in { + val types = List( + LiteralType.signed, + LiteralType.unsigned + ) + + allPairs(types).foreach { case (lt, rt) => + val llit = literal("42", lt) + val rlit = literal("37", rt) + + val alg = algebra() + + InfixToken.Op.math + .filterNot( + // Can not use negative numbers with pow + _ == InfixToken.Op.Pow && rt != LiteralType.unsigned + ) + .foreach { op => + val token = InfixToken[Id](llit, rlit, op) + + val (st, res) = alg + .valueToRaw(token) + .run(genState()) + .value + + val t = if (lt == rt) lt else LiteralType.signed + + inside(res) { case Some(value) => + value.`type` shouldBe t + } + } + } + } + + it should "handle +, -, /, *, % on number vars" in { + allPairs(ScalarType.integer.toList).foreach { case (lt, rt) => + val vl = variable("left") + val vr = variable("right") + + val ut = lt.uniteTop(rt) + + val state = genState( + vars = Map( + "left" -> lt, + "right" -> rt + ) + ) + + val alg = algebra() + + InfixToken.Op.math + .filterNot( + // Can not use negative numbers with pow + _ == InfixToken.Op.Pow && ScalarType.signed(rt) + ) + .foreach { op => + val token = InfixToken[Id](vl, vr, op) + + val (st, res) = alg + .valueToRaw(token) + .run(state) + .value + + inside(res) { case Some(value) => + value.`type` shouldBe a[ScalarType] + + if (ut != TopType) { + value.`type`.acceptsValueOf(lt) shouldBe true + value.`type`.acceptsValueOf(rt) shouldBe true + } else { + // This should happen only if + // of the types is 64 bit + List(lt, rt).exists( + List(ScalarType.u64, ScalarType.i64).contains + ) shouldBe true + + (value.`type`.acceptsValueOf(lt) || + value.`type`.acceptsValueOf(rt)) shouldBe true + } + + } + } + } + } + + it should "handle * on float literals" in { + val llit = literal("42.1", LiteralType.float) + val rlit = literal("37.2", LiteralType.float) + + val alg = algebra() + + val token = InfixToken[Id](llit, rlit, InfixToken.Op.Mul) + + val (st, res) = alg + .valueToRaw(token) + .run(genState()) + .value + + inside(res) { case Some(value) => + value.`type` shouldBe ScalarType.i64 + } + } + + it should "handle * on float vars" in { + allPairs(ScalarType.float.toList).foreach { case (lt, rt) => + val lvar = variable("left") + val rvar = variable("right") + + val alg = algebra() + + val state = genState( + vars = Map( + "left" -> lt, + "right" -> rt + ) + ) + + val token = InfixToken[Id](lvar, rvar, InfixToken.Op.Mul) + + val (st, res) = alg + .valueToRaw(token) + .run(state) + .value + + inside(res) { case Some(value) => + value.`type` shouldBe ScalarType.i64 + } + } + } +} diff --git a/types/src/main/scala/aqua/types/CompareTypes.scala b/types/src/main/scala/aqua/types/CompareTypes.scala index 6ad257696..74a9e9218 100644 --- a/types/src/main/scala/aqua/types/CompareTypes.scala +++ b/types/src/main/scala/aqua/types/CompareTypes.scala @@ -110,8 +110,7 @@ object CompareTypes { case _ if l == r => 0.0 case (TopType, _) | (_, BottomType) => 1.0 - case (BottomType, _) | (_, TopType) => - -1.0 + case (BottomType, _) | (_, TopType) => -1.0 // Collections case (x: ArrayType, y: ArrayType) => apply(x.element, y.element) diff --git a/types/src/main/scala/aqua/types/Type.scala b/types/src/main/scala/aqua/types/Type.scala index c9d99153c..953c26a77 100644 --- a/types/src/main/scala/aqua/types/Type.scala +++ b/types/src/main/scala/aqua/types/Type.scala @@ -186,7 +186,11 @@ case class LiteralType private (oneOf: Set[ScalarType], name: String) extends Da object LiteralType { val float = LiteralType(ScalarType.float, "float") val signed = LiteralType(ScalarType.signed, "signed") - val unsigned = LiteralType(ScalarType.unsigned, "unsigned") + /* + * Literals without sign could be either signed or unsigned + * so `ScalarType.integer` is used here + */ + val unsigned = LiteralType(ScalarType.integer, "unsigned") val number = LiteralType(ScalarType.number, "number") val bool = LiteralType(Set(ScalarType.bool), "bool") val string = LiteralType(Set(ScalarType.string), "string") @@ -236,7 +240,8 @@ sealed trait NamedType extends Type { } // Struct is an unordered collection of labelled types -case class StructType(name: String, fields: NonEmptyMap[String, Type]) extends DataType with NamedType { +case class StructType(name: String, fields: NonEmptyMap[String, Type]) + extends DataType with NamedType { override def toString: String = s"$name{${fields.map(_.toString).toNel.toList.map(kv => kv._1 + ": " + kv._2).mkString(", ")}}" @@ -246,11 +251,11 @@ case class StructType(name: String, fields: NonEmptyMap[String, Type]) extends D case class AbilityType(name: String, fields: NonEmptyMap[String, Type]) extends NamedType { lazy val arrows: Map[String, ArrowType] = fields.toNel.collect { - case (name, at@ArrowType(_, _)) => (name, at) + case (name, at @ ArrowType(_, _)) => (name, at) }.toMap lazy val abilities: List[(String, AbilityType)] = fields.toNel.collect { - case (name, at@AbilityType(_, _)) => (name, at) + case (name, at @ AbilityType(_, _)) => (name, at) } lazy val variables: List[(String, Type)] = fields.toNel.filter {