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 e2aa235..cdfb344 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 @@ -12,7 +12,7 @@ object GenerativeFluentAssertionsDSL: import quotes.reflect.* subject match { - case '{ $subject: Iterable[Any] } => '{ ContainsVerb($subject) } + case '{ $subject: Iterable[elementType] } => '{ ContainsVerb($subject) } case _ => report.errorAndAbort( "assertion subject must be an Iterable[?] in order to use a 'contain' assertion", subject @@ -45,6 +45,13 @@ object GenerativeFluentAssertionsDSL: .applyDynamic($propertyNameExpr)(satisfying.notEquals[T]($obj, $ord)) .$asInstanceOf$[AssertionProperty] } + case '{ (${ typeProvider }: ContainsPropertyTypeProvider[subjectType]) + .applyDynamic($propertyNameExpr: String)(satisfying) + .$asInstanceOf$[SatisfyingNotEquals[T]] } => '{ + $typeProvider + .applyDynamic($propertyNameExpr)(satisfying.notEquals[T]($obj, $ord)) + .$asInstanceOf$[AssertionProperty] + } } /** Entrypoint. This macro method takes an expression composed of @@ -124,7 +131,6 @@ object GenerativeFluentAssertionsDSL: .applyDynamic($propertyNameExpr: String)($valueExpr: Condition[valueType]) .$asInstanceOf$[AssertionProperty] } => - println(assertionExpr.show) val subjectExpr = '{ $typeProvider.subject } val negatedExpr = '{ $typeProvider.negated } generateShouldHaveAssertion[subjectType, valueType]( @@ -145,7 +151,14 @@ object GenerativeFluentAssertionsDSL: .$asInstanceOf$[AssertionProperty] } => '{} // placeholder - case _ => + case '{ + (${ typeProvider }: ContainsPropertyTypeProvider[subjectType]) + .applyDynamic($propertyNameExpr: String)($valueExpr: Condition[valueType]) + .$asInstanceOf$[AssertionProperty] + } => + '{} // placeholder + case a => + println(a.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 c914e1a..4c8c481 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 @@ -1,6 +1,7 @@ package ch.usi.si.msde.edsl.lecture10 import scala.language.dynamics +import scala.quoted.Expr sealed trait AssertionProperty case class NegatedAssertionProperty(prop: AssertionProperty) extends AssertionProperty @@ -67,17 +68,19 @@ case object have extends HaveVerb(false) case object contain extends AssertionDSLVerb case object `with` extends AssertionDSLVerb -case class ContainsVerb[T](subject: Iterable[T]): - transparent inline def allElements(verb: `with`.type) = ${ - ShouldContainTypeProvider.generateShouldContainTypeProvider[T]('subject, 'All) +case class ContainsVerb[T](subject: Iterable[T]) + +extension [T](inline containsVerb: ContainsVerb[T]) + transparent inline def allElements(inline verb: `with`.type) = ${ + ShouldContainTypeProvider.generateShouldContainTypeProvider[T]('containsVerb, 'All) } - transparent inline def someElements(verb: `with`.type) = ${ - ShouldContainTypeProvider.generateShouldContainTypeProvider[T]('subject, 'Some) + transparent inline def someElements(inline verb: `with`.type) = ${ + ShouldContainTypeProvider.generateShouldContainTypeProvider[T]('containsVerb, 'Some) } - transparent inline def noElements(verb: `with`.type) = ${ - ShouldContainTypeProvider.generateShouldContainTypeProvider[T]('subject, 'None) + transparent inline def noElements(inline verb: `with`.type) = ${ + ShouldContainTypeProvider.generateShouldContainTypeProvider[T]('containsVerb, 'None) } /** Implicit class to add 'should' assertions to a generic type T. @@ -88,6 +91,13 @@ case class ContainsVerb[T](subject: Iterable[T]): extension [T](subject: T) def should(property: AssertionProperty) = ??? +// we use the 'inline' modifier on subject as well to make sure that the expression +// generated by the 'shouldContainImpl' macro is a one-liner, so it does not contain +// intermediate '$proxy' variable declarations that would make expression pattern +// matching impossible. Using the 'inline' modifier requires all extension methods +// to be 'inline', so we separate the original 'should be' implementation in the +// extension methods block above. +extension [T](inline subject: T) transparent inline def should(inline verbWord: be_.type) = ${ ShouldBeTypeProvider.generateShouldBeTypeProvider[T]('subject) } @@ -109,7 +119,13 @@ extension [T](subject: T) transparent inline def should(inline verb: HaveVerb) = ${ ShouldHaveTypeProvider.generateShouldHaveTypeProvider[T]('subject, '{verb.negated}) } - - transparent inline def should(inline verb: contain.type) = ${ - GenerativeFluentAssertionsDSL.shouldContainImpl('subject) + + // We define here the 'should contain' extension method. It is not possible to + // write an extension method for Iterable[T] as that method would shadow the + // 'should be' and 'should have' assertions defined for all objects. Therefore, + // we check the subject is an instance of Iterable[T] with a macro, and we use + // report.errorAndAbort to provide a clear error message stating that an Iterable[T] + // is required for this kind of assertion. + transparent inline def should(inline verb: contain.type) = ${ + GenerativeFluentAssertionsDSL.shouldContainImpl('subject) } \ No newline at end of file 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 306ebf0..3cba76d 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 @@ -2,11 +2,11 @@ package ch.usi.si.msde.edsl.lecture10 // import scala.language.postfixOps -import squants.mass.MassConversions.MassConversions -import squants.market.MoneyConversions.MoneyConversions -import GenerativeFluentAssertionsDSL.* -import squants.mass.Mass +import ch.usi.si.msde.edsl.lecture10.GenerativeFluentAssertionsDSL.* import squants.market.Money +import squants.market.MoneyConversions.MoneyConversions +import squants.mass.Mass +import squants.mass.MassConversions.MassConversions extension (value: Double) def i = Complex(0, value) @@ -47,6 +47,11 @@ case class Box(label: String, weight: Mass, price: Money) List(50, 2, 3) should have head satisfying < 100 box should !(!have) weight satisfying <= 300.0.kg +// List("bar","foo","","alice") should contain someElements `with` size satisfying === 0 + List(Seq(), Seq(), Seq()) should contain allElements `with` size satisfying >= 0 +// List(Seq(), Seq(), Seq()) should contain allElements `with` size satisfying !== 0 + List(List(1,2), List(20,1), List(3,4)) should contain noElements `with` head satisfying === 3 + /* should have assertions */ // // assert(List(1).head == 1, ...) // List(1) should have head 1 diff --git a/src/main/scala/ch/usi/si/msde/edsl/lecture10/ShouldBeTypeProvider.scala b/src/main/scala/ch/usi/si/msde/edsl/lecture10/ShouldBeTypeProvider.scala index 9b679c9..4f2f379 100644 --- a/src/main/scala/ch/usi/si/msde/edsl/lecture10/ShouldBeTypeProvider.scala +++ b/src/main/scala/ch/usi/si/msde/edsl/lecture10/ShouldBeTypeProvider.scala @@ -53,8 +53,6 @@ object ShouldBeTypeProvider: members ) -// println(refinedType) - // This is the way to extract a (reflection) type to a // type that can be used in some quoted code. // it's the equivalent of .asExpr for expressions, @@ -66,7 +64,6 @@ object ShouldBeTypeProvider: val p = BePropertyTypeProvider[T]($subject) p.asInstanceOf[tpe] } - println(res.show) res end ShouldBeTypeProvider diff --git a/src/main/scala/ch/usi/si/msde/edsl/lecture10/ShouldContainTypeProvider.scala b/src/main/scala/ch/usi/si/msde/edsl/lecture10/ShouldContainTypeProvider.scala index 475d385..152beab 100644 --- a/src/main/scala/ch/usi/si/msde/edsl/lecture10/ShouldContainTypeProvider.scala +++ b/src/main/scala/ch/usi/si/msde/edsl/lecture10/ShouldContainTypeProvider.scala @@ -7,11 +7,16 @@ object ShouldContainTypeProvider: /** The type provider for the should have assertion, which generates a refined * structural type for type T. */ - def generateShouldContainTypeProvider[T](subject: Expr[Iterable[T]], kind: Expr[ContainsAssertionKind])(using Type[T])(using - Quotes - ): Expr[Any] = + def generateShouldContainTypeProvider[T](containsExpr: Expr[ContainsVerb[T]], kind: Expr[ContainsAssertionKind]) + (using Type[T], Quotes): Expr[Any] = import quotes.reflect.* + println("containsExpr: " + containsExpr.show) + println("kind: " + kind.show) + + val subject: Expr[Iterable[T]] = containsExpr match + case '{ ContainsVerb.apply[T]($s: Iterable[T]) } => s + // 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 @@ -19,7 +24,10 @@ object ShouldContainTypeProvider: def typeConstructor[U](using Type[U]): TypeRepr = AppliedType.unapply(TypeRepr.of[U].asInstanceOf[AppliedType])._1 - val subjectTypeRepr = TypeRepr.of[T] + val subjectTypeRepr = TypeRepr.of[T].widen + + println("subjectTypeRepr: " + subjectTypeRepr) + println("string: " + TypeRepr.of[String].simplified) /** Given a refinable current type, and the symbol of a arity-0 method or a * field of type foo: X, generates a refinement containing a method with @@ -72,12 +80,15 @@ object ShouldContainTypeProvider: Flags.Synthetic ) + println("typeSymbol: " + subjectTypeRepr.typeSymbol) + println("fields: " + fields) + println("arityZeroMethods: " + arityZeroMethods) + val refinedType = refineTypeBySymbols( TypeRepr.of[ContainsPropertyTypeProvider[T]], fields ++ arityZeroMethods ) - println(refinedType.show) // This is the way to extract a (reflection) type to a // type that can be used in some quoted code. @@ -86,6 +97,8 @@ object ShouldContainTypeProvider: // exact type is unknown until compilation time. refinedType.asType match case '[tpe] => // tpe is the exact refined type - '{ ContainsPropertyTypeProvider[T]($subject, $kind).asInstanceOf[tpe] } + val a = '{ ContainsPropertyTypeProvider[T]($subject, $kind).asInstanceOf[tpe] } + println(a.show) + a end ShouldContainTypeProvider 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 5ebaeb3..c678699 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 @@ -77,8 +77,6 @@ object ShouldHaveTypeProvider: fields ++ arityZeroMethods ) - println(refinedType.show) - // This is the way to extract a (reflection) type to a // type that can be used in some quoted code. // it's the equivalent of .asExpr for expressions,