From cbe868975b4085d79704644335e2c1bd237a8016 Mon Sep 17 00:00:00 2001 From: Claudio Maggioni Date: Sun, 22 Dec 2024 16:21:16 +0100 Subject: [PATCH] ex4 started --- .../GenerativeFluentAssertionsDSL.scala | 12 +++ .../GenerativeFluentAssertionsDSLSyntax.scala | 39 ++++++-- .../lecture10/ShouldContainTypeProvider.scala | 91 +++++++++++++++++++ 3 files changed, 133 insertions(+), 9 deletions(-) create mode 100644 src/main/scala/ch/usi/si/msde/edsl/lecture10/ShouldContainTypeProvider.scala 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 28ec9b5..e2aa235 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 @@ -8,6 +8,18 @@ import scala.language.postfixOps */ object GenerativeFluentAssertionsDSL: + def shouldContainImpl[T](subject: Expr[T])(using Type[T])(using Quotes): Expr[Any] = { + import quotes.reflect.* + + subject match { + case '{ $subject: Iterable[Any] } => '{ ContainsVerb($subject) } + case _ => report.errorAndAbort( + "assertion subject must be an Iterable[?] in order to use a 'contain' assertion", + subject + ) + } + } + extension (inline property: AssertionProperty) inline def `unary_!`: AssertionProperty = NegatedAssertionProperty(property) 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 0370895..c914e1a 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 @@ -42,6 +42,16 @@ class HavePropertyTypeProvider[T](val subject: T, val negated: Boolean) extends def applyDynamic(fieldName: String)(arg: Any): AssertionProperty = ??? end HavePropertyTypeProvider +sealed trait ContainsAssertionKind +case object All extends ContainsAssertionKind +case object None extends ContainsAssertionKind +case object Some extends ContainsAssertionKind + +class ContainsPropertyTypeProvider[T](val subject: Iterable[T], val kind: ContainsAssertionKind) extends Selectable: + def applyDynamic[U](fieldName: String)(arg: satisfying.type): SatisfyingNotEquals[U] = ??? + def applyDynamic(fieldName: String)(arg: Any): AssertionProperty = ??? +end ContainsPropertyTypeProvider + /** A trait for each type of assertion (be or have). */ sealed trait AssertionDSLVerb @@ -54,21 +64,28 @@ sealed trait HaveVerb(val negated: Boolean): case class HaveNegation(haveVerb: HaveVerb) extends HaveVerb(!haveVerb.negated) 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) + } + + transparent inline def someElements(verb: `with`.type) = ${ + ShouldContainTypeProvider.generateShouldContainTypeProvider[T]('subject, 'Some) + } + + transparent inline def noElements(verb: `with`.type) = ${ + ShouldContainTypeProvider.generateShouldContainTypeProvider[T]('subject, 'None) + } + /** Implicit class to add 'should' assertions to a generic type T. * * @param subject * the object to which perform the assertion. */ extension [T](subject: T) - /** Specifies a 'be' assertion. The method is just a placeholder to fix the - * syntax, its semantics is performed by the assertions macro. - * - * @param verb - * The be word. - * @return - * a DynamicProperty object which allows to specify a further property - * name. - */ def should(property: AssertionProperty) = ??? transparent inline def should(inline verbWord: be_.type) = ${ @@ -92,3 +109,7 @@ 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) + } \ No newline at end of file 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 new file mode 100644 index 0000000..475d385 --- /dev/null +++ b/src/main/scala/ch/usi/si/msde/edsl/lecture10/ShouldContainTypeProvider.scala @@ -0,0 +1,91 @@ +package ch.usi.si.msde.edsl.lecture10 + +import scala.quoted.* + +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] = + 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 + * field of type foo: X, generates a refinement containing a method with + * signature def foo(arg: X): AssertionProperty. + */ + def refineTypeBySymbol( + currentTypeRepr: TypeRepr, + symbol: Symbol + ): TypeRepr = + // The type of the field, or the return type of the method. + val fieldTypeRepr = subjectTypeRepr.memberType(symbol).widen + + val methodType = MethodType(List("arg"))( + _ => List(AppliedType(typeConstructor[Condition[?]], List(fieldTypeRepr))), + _ => TypeRepr.of[AssertionProperty] + ) + + 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. + */ + def refineTypeBySymbols( + currentTypeRepr: TypeRepr, + fields: List[Symbol] + ): TypeRepr = + fields match + // this is pattern matching on list - like head :: rest + case symbol :: symbols => + refineTypeBySymbols( + refineTypeBySymbol(currentTypeRepr, symbol), + symbols + ) + // empty list case + case Nil => + currentTypeRepr + + val fields = subjectTypeRepr.typeSymbol.fieldMembers + val arityZeroMethods = subjectTypeRepr.typeSymbol.methodMembers + .filter: methodMember => + methodMember.paramSymss.size == 0 && !methodMember.flags.is( + Flags.Synthetic + ) + + 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. + // it's the equivalent of .asExpr for expressions, + // but it's more complicated because in this case the + // exact type is unknown until compilation time. + refinedType.asType match + case '[tpe] => // tpe is the exact refined type + '{ ContainsPropertyTypeProvider[T]($subject, $kind).asInstanceOf[tpe] } + +end ShouldContainTypeProvider