diff --git a/build.sbt b/build.sbt index e69e0a3..6d72c2c 100644 --- a/build.sbt +++ b/build.sbt @@ -4,7 +4,7 @@ lazy val root = project .in(file(".")) .settings( organization := "ch.usi.si.msde.edsl", - name := "lecture-10", + name := "assignment-04a", version := "2024.01", scalaVersion := scala3Version, libraryDependencies += "org.typelevel" %% "squants" % "1.8.3", diff --git a/src/main/scala/ch/usi/si/msde/edsl/lecture10/GenerativeFluentAssertionsDSL.scala b/src/main/scala/ch/usi/si/msde/edsl/lecture10/GenerativeFluentAssertionsDSL.scala index 165d4d9..668c087 100644 --- a/src/main/scala/ch/usi/si/msde/edsl/lecture10/GenerativeFluentAssertionsDSL.scala +++ b/src/main/scala/ch/usi/si/msde/edsl/lecture10/GenerativeFluentAssertionsDSL.scala @@ -70,9 +70,16 @@ object GenerativeFluentAssertionsDSL: // the should have assertion. Again, the applyDynamic is explicit. // The quoted pattern also extracts: the type of the assertion subject, // and the type of the property (actually... not exactly that. Why?) + case '{ + (${ typeProvider }: HavePropertyTypeProvider[subjectType]) + .applyDynamic($propertyNameExpr: String)(satisfying) + .$asInstanceOf$[SatisfyingNotEquals[fieldType]] + .`!==`($property) + } => + '{} case '{ (${ typeProvider }: HavePropertyTypeProvider[subjectType]) - .applyDynamic($propertyNameExpr)($valueExpr: valueType => Boolean) + .applyDynamic($propertyNameExpr: String)($valueExpr: valueType => Boolean) .$asInstanceOf$[AssertionProperty] } => val subjectExpr = '{ $typeProvider.subject } @@ -94,6 +101,7 @@ object GenerativeFluentAssertionsDSL: } => '{} // placeholder case _ => + println(assertionExpr.show) report.errorAndAbort( "Invalid expression, must be a 'should be' or 'should have' assertion. ", assertionExpr diff --git a/src/main/scala/ch/usi/si/msde/edsl/lecture10/GenerativeFluentAssertionsDSLSyntax.scala b/src/main/scala/ch/usi/si/msde/edsl/lecture10/GenerativeFluentAssertionsDSLSyntax.scala index 83786ae..eaa57c8 100644 --- a/src/main/scala/ch/usi/si/msde/edsl/lecture10/GenerativeFluentAssertionsDSLSyntax.scala +++ b/src/main/scala/ch/usi/si/msde/edsl/lecture10/GenerativeFluentAssertionsDSLSyntax.scala @@ -12,11 +12,17 @@ class DynamicShouldBeProperty extends AssertionProperty with Dynamic: def applyDynamic(fieldName: String)(foo: Any*) = ??? end DynamicShouldBeProperty -object satisfying: +/** + * Due to operator precedence, '!==' is the only comparison operator that is considered an "assignment operation" + * and thus has lower precedence than alphanumeric operators. Therefore, we must handle "!==" as the final link of + * a method chain instead of a predicate builder on `satisfying`, like the other operators. + */ +sealed trait SatisfyingNotEquals[T]: + def `!==`(toWhat: T): AssertionProperty = ??? + +case object satisfying: def `===`[T](toWhat: T)(using ord: Ordering[T]): T => Boolean = something => ord.eq(something, toWhat) - def `!==`[T](toWhat: T)(using ord: Ordering[T]): T => Boolean = - something => ord.ne(something, toWhat) def `<`[T](toWhat: T)(using ord: Ordering[T]): T => Boolean = something => ord.lt(something, toWhat) def `>`[T](toWhat: T)(using ord: Ordering[T]): T => Boolean = @@ -25,7 +31,6 @@ object satisfying: something => ord.lteq(something, toWhat) def `>=`[T](toWhat: T)(using ord: Ordering[T]): T => Boolean = something => ord.gteq(something, toWhat) -end satisfying class BePropertyTypeProvider[T](val subject: T) extends Selectable: def selectDynamic(fieldName: String): AssertionProperty = ??? @@ -44,6 +49,7 @@ end BePropertyTypeProvider * corresponding method def foo(arg: X): AssertionProperty. */ class HavePropertyTypeProvider[T](val subject: T) extends Selectable: + def applyDynamic[U](fieldName: String)(arg: satisfying.type): SatisfyingNotEquals[U] = ??? def applyDynamic(fieldName: String)(arg: Any): AssertionProperty = ??? end HavePropertyTypeProvider diff --git a/src/main/scala/ch/usi/si/msde/edsl/lecture10/Main.scala b/src/main/scala/ch/usi/si/msde/edsl/lecture10/Main.scala index a926744..3aeb7b6 100644 --- a/src/main/scala/ch/usi/si/msde/edsl/lecture10/Main.scala +++ b/src/main/scala/ch/usi/si/msde/edsl/lecture10/Main.scala @@ -4,7 +4,6 @@ package ch.usi.si.msde.edsl.lecture10 import squants.mass.MassConversions.MassConversions import squants.market.MoneyConversions.MoneyConversions - import GenerativeFluentAssertionsDSL.* import squants.mass.Mass import squants.market.Money @@ -25,13 +24,6 @@ case class Box(label: String, weight: Mass, price: Money) val box = Box("aBox", 30.kg, 10.CHF) assertions: - // { - // val p: BePropertyTypeProvider[Person] = new BePropertyTypeProvider[Person](person) - // p.asInstanceOf[BePropertyTypeProvider[Person] { - // def adult: AssertionProperty - // }].adult - // } - /* be.property assertions */ // assert(person.adult, ...) person should be.adult @@ -48,7 +40,14 @@ case class Box(label: String, weight: Mass, price: Money) // or adult is a property but braces are needed to resolve the type provider (person should be_).adult - /* should have assertions */ + List(1) should have head satisfying === 1 + box should have weight satisfying === 3.kg + List() should have size satisfying >= 0 + List(3) should have size satisfying !== 0 + List(50, 2, 3) should have head satisfying < 100 + box should have weight satisfying <= 3.0.kg + +/* should have assertions */ // // assert(List(1).head == 1, ...) // List(1) should have head 1 // // assert(box.weight == 30.kg, ...) diff --git a/src/main/scala/ch/usi/si/msde/edsl/lecture10/ShouldHaveTypeProvider.scala b/src/main/scala/ch/usi/si/msde/edsl/lecture10/ShouldHaveTypeProvider.scala index d2f7adf..b7dc705 100644 --- a/src/main/scala/ch/usi/si/msde/edsl/lecture10/ShouldHaveTypeProvider.scala +++ b/src/main/scala/ch/usi/si/msde/edsl/lecture10/ShouldHaveTypeProvider.scala @@ -12,6 +12,13 @@ object ShouldHaveTypeProvider: ): Expr[Any] = import quotes.reflect.* + // hack to get the equivalent of `TypeRepr.of[Function1[?, ?]]` (i.e. an arity-1 function type constructor). + // Getting it as is returns an applied type with useless bounds (Nothing to Any), and the returned TypeRepr, + // if re-applied to some `I` input type and `O` output type would be incompatible with `TypeRepr.of[I => O]` for + // some reason + def typeConstructor[U](using Type[U]): TypeRepr = + AppliedType.unapply(TypeRepr.of[U].asInstanceOf[AppliedType])._1 + val subjectTypeRepr = TypeRepr.of[T] /** Given a refinable current type, and the symbol of a arity-0 method or a @@ -25,30 +32,22 @@ object ShouldHaveTypeProvider: // The type of the field, or the return type of the method. val fieldTypeRepr = subjectTypeRepr.memberType(symbol).widen - // hack to get the equivalent of `TypeRepr.of[Function1[?, ?]]` (i.e. an arity-1 function type constructor). - // Getting it as is returns an applied type with useless bounds (Nothing to Any), and the returned TypeRepr, - // if re-applied to some `I` input type and `O` output type would be incompatible with `TypeRepr.of[I => O]` for - // some reason - val function1TypeConstructor = AppliedType.unapply(TypeRepr.of[? => ?].asInstanceOf[AppliedType])._1 - - val appliedType = AppliedType(function1TypeConstructor, List(fieldTypeRepr, TypeRepr.of[Boolean])) - - // Generates the "type" of the method to be generated for the refinement. - // The first parameter is the list of arguments, the second is a function returning - // the type of arguments, and the third one is a function returning the return type - // of the method. - // In this case: arg is the name of the parameter; - // _ => List(fieldTypeRepr) returns a list with the type of arg; - // _ => TypeRepr.of[AssertionProperty] returns the (reflection) type of the method. + val appliedType = AppliedType(typeConstructor[? => ?], List(fieldTypeRepr, TypeRepr.of[Boolean])) val methodType = MethodType(List("arg"))( _ => List(appliedType), _ => TypeRepr.of[AssertionProperty] - ).widen - // returns the refinement of currentTypeRepr - - // symbol.name is the name of the method, - // methodType is its type. - val refinement = Refinement(currentTypeRepr, symbol.name, methodType) - refinement + ) + + val chainedMethodType = MethodType(List("arg"))( + _ => List(TypeRepr.of[satisfying.type]), + _ => AppliedType(typeConstructor[SatisfyingNotEquals[?]], List(fieldTypeRepr)) + ) + + Refinement( + Refinement(currentTypeRepr, symbol.name, chainedMethodType), + symbol.name, + methodType + ) /** Refines a type according to a list of fields or methods of arity 0. */