Compare commits

...

11 commits
ex2 ... main

Author SHA1 Message Date
0b9bff5ba8 ready for submission 2024-12-22 23:11:09 +01:00
f73a8601c3 almost done 2024-12-22 22:22:29 +01:00
decd52836f works 2024-12-22 22:19:25 +01:00
e89a95f88e promising bonus 2024-12-22 21:03:07 +01:00
601826c403 no bonus 2024-12-22 19:07:36 +01:00
efa648207f ex4 predef string weird error 2024-12-22 17:33:19 +01:00
cbe868975b ex4 started 2024-12-22 16:21:16 +01:00
07f5451064 ex3 done 2024-12-18 21:55:31 +01:00
16b4367ed0 ex1 and ex2 done 2024-12-18 14:17:26 +01:00
fa3972ec1b ex2 works 2024-12-18 14:01:08 +01:00
dda2d1b61f compiles 2024-12-18 12:52:31 +01:00
8 changed files with 423 additions and 154 deletions

View file

@ -1,3 +1,23 @@
## Lecture 12 # Assignment 03
Code for Lecture 12. Student: Claudio Maggioni
## Use of AI assistants / LLMs
I declare that I used the following AI assistants / LLMs (put an X where needed):
- [X] ChatGPT (GPT-4o mini)
Briefly, how helpful was the AI assistant for this assignment?
- assistance on how to pattern match complex quoted expressions for 'should have' and 'should contain' assertions
- assistance on how to build an AST for a lambda expression
## Exercises Completed
Please Mark the exercises that you chose/completed:
- [X] Exercise 1
- [X] Exercise 2
- [X] Exercise 3
- [X] Exercise 4 (with bonus)

View file

@ -4,7 +4,7 @@ lazy val root = project
.in(file(".")) .in(file("."))
.settings( .settings(
organization := "ch.usi.si.msde.edsl", organization := "ch.usi.si.msde.edsl",
name := "lecture-10", name := "assignment-04a",
version := "2024.01", version := "2024.01",
scalaVersion := scala3Version, scalaVersion := scala3Version,
libraryDependencies += "org.typelevel" %% "squants" % "1.8.3", libraryDependencies += "org.typelevel" %% "squants" % "1.8.3",

View file

@ -8,6 +8,52 @@ import scala.language.postfixOps
*/ */
object GenerativeFluentAssertionsDSL: object GenerativeFluentAssertionsDSL:
def shouldContainImpl[T](subject: Expr[T])(using Type[T])(using Quotes): Expr[Any] = {
import quotes.reflect.*
subject match {
case '{ $subject: Iterable[elementType] } => '{ 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)
/**
* 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.
*/
extension [T](inline property: SatisfyingNotEquals[T])
inline def `!==`(inline obj: T)(using inline ord: Ordering[T]): AssertionProperty =
${ notEqualsImpl('property, 'obj, 'ord) }
private def notEqualsImpl[T: Type](property: Expr[SatisfyingNotEquals[T]], obj: Expr[T], ord: Expr[Ordering[T]])(
using Quotes
): Expr[AssertionProperty] = {
import quotes.reflect.*
property match
case '{ (${ typeProvider }: HavePropertyTypeProvider[subjectType])
.applyDynamic($propertyNameExpr: String)(satisfying)
.$asInstanceOf$[SatisfyingNotEquals[T]] } => '{
$typeProvider
.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 /** Entrypoint. This macro method takes an expression composed of
* AssertionProperty expressions and rewrites them into actual assertions. * AssertionProperty expressions and rewrites them into actual assertions.
*/ */
@ -15,6 +61,19 @@ object GenerativeFluentAssertionsDSL:
assertionsGenerativeImpl('assertions) assertionsGenerativeImpl('assertions)
} }
private def unwrapShouldBeNegatedAssertion(assertionExpr: Expr[AssertionProperty], negated: Boolean = false)(
using Quotes
): (Expr[String], Boolean) =
import quotes.reflect.*
assertionExpr match
case '{ NegatedAssertionProperty($innerExpr: AssertionProperty) } => unwrapShouldBeNegatedAssertion(innerExpr, !negated)
case '{ be.selectDynamic($propertyNameExpr: String) } => (propertyNameExpr, negated)
case what => report.errorAndAbort(
"'be' negation unwrapping: Invalid expression, must be a 'should be' assertion.",
what
)
/** Macro implementation. The method is recursive, each single expression is /** Macro implementation. The method is recursive, each single expression is
* then separately rewritten into the generateAssertion method. The method * then separately rewritten into the generateAssertion method. The method
* returns a compiler error if the expression is not an AssertionProperty. * returns a compiler error if the expression is not an AssertionProperty.
@ -60,40 +119,46 @@ object GenerativeFluentAssertionsDSL:
val generatedAssertion = assertionExpr match val generatedAssertion = assertionExpr match
// be assertion. Check the explicit selectDynamic to extract the property name // be assertion. Check the explicit selectDynamic to extract the property name
case '{ case '{ (${ subjectExpr }: subjectType) should ($assertionProperty: AssertionProperty) } =>
(${ subjectExpr }: subjectType) should be.selectDynamic( val (propertyNameExpr, negated) = unwrapShouldBeNegatedAssertion(assertionProperty)
$propertyNameExpr generateShouldBeAssertion[subjectType](subjectExpr, propertyNameExpr, negated)
)
} =>
generateShouldBeAssertion[subjectType](subjectExpr, propertyNameExpr)
// The more complicated structure of property type provider construction in // The more complicated structure of property type provider construction in
// the should have assertion. Again, the applyDynamic is explicit. // the should have assertion. Again, the applyDynamic is explicit.
// The quoted pattern also extracts: the type of the assertion subject, // The quoted pattern also extracts: the type of the assertion subject,
// and the type of the property (actually... not exactly that. Why?) // and the type of the property (actually... not exactly that. Why?)
case '{ case '{
(${ typeProvider }: HavePropertyTypeProvider[subjectType]) (${ typeProvider }: HavePropertyTypeProvider[subjectType])
.applyDynamic($propertyNameExpr)($valueExpr: valueType => Boolean) .applyDynamic($propertyNameExpr: String)($valueExpr: Condition[valueType])
.$asInstanceOf$[AssertionProperty] .$asInstanceOf$[AssertionProperty]
} => } =>
val subjectExpr = '{ $typeProvider.subject } val subjectExpr = '{ $typeProvider.subject }
val checkerExpr = '{ $typeProvider.checker }
generateShouldHaveAssertion[subjectType, valueType]( generateShouldHaveAssertion[subjectType, valueType](
subjectExpr, subjectExpr,
checkerExpr,
propertyNameExpr,
valueExpr
)
case '{
(${ typeProvider }: ContainsPropertyTypeProvider[subjectType])
.applyDynamic($propertyNameExpr: String)($valueExpr: Condition[valueType])
.$asInstanceOf$[AssertionProperty]
} =>
val subjectExpr = '{ $typeProvider.subject }
val checkerExpr = '{ $typeProvider.checker }
generateShouldContainAssertion[subjectType, valueType](
subjectExpr,
checkerExpr,
propertyNameExpr, propertyNameExpr,
valueExpr valueExpr
) )
case '{ case '{
(${ typeProvider }: BePropertyTypeProvider[subjectType]) (${ typeProvider }: BePropertyTypeProvider[subjectType])
.applyDynamic($propertyNameExpr)($unit) .applyDynamic($propertyNameExpr)($end)
.$asInstanceOf$[AssertionProperty] .$asInstanceOf$[AssertionProperty]
} => } =>
'{} // placeholder '{} // placeholder
case '{ case a =>
(${ typeProvider }: BePropertyTypeProvider[subjectType])
.selectDynamic($propertyNameExpr)
.$asInstanceOf$[AssertionProperty]
} =>
'{} // placeholder
case _ =>
report.errorAndAbort( report.errorAndAbort(
"Invalid expression, must be a 'should be' or 'should have' assertion. ", "Invalid expression, must be a 'should be' or 'should have' assertion. ",
assertionExpr assertionExpr
@ -104,7 +169,8 @@ object GenerativeFluentAssertionsDSL:
private def generateShouldBeAssertion[T]( private def generateShouldBeAssertion[T](
subject: Expr[T], subject: Expr[T],
propertyNameExpr: Expr[String] propertyNameExpr: Expr[String],
negated: Boolean
)(using Type[T])(using Quotes): Expr[Unit] = )(using Type[T])(using Quotes): Expr[Unit] =
import quotes.reflect.* import quotes.reflect.*
@ -127,27 +193,30 @@ object GenerativeFluentAssertionsDSL:
// Constructs a 'select' tree, that is, a selection to the field or method // Constructs a 'select' tree, that is, a selection to the field or method
// of the subject. // of the subject.
val call = Select(subject.asTerm, validSymbol).asExprOf[Boolean] val call = Select(subject.asTerm, validSymbol).asExprOf[Boolean]
val expected = '{ !${Expr(negated)} }
val assertion = '{ val assertion = '{
assert( assert(
$call, $call == $expected,
s"${$subject} was not ${$propertyNameExpr} (${$validSymbolNameExpr} is false)" s"${$subject} was${if ($expected) " not" else ""} ${$propertyNameExpr} (${$validSymbolNameExpr} is ${if ($expected) "false" else "true"})"
) )
} }
assertion assertion
end if end if
end generateShouldBeAssertion end generateShouldBeAssertion
private def generateShouldHaveAssertion[T, R]( private def generateShouldHaveAssertion[E, R](
subjectExpr: Expr[T], subjectExpr: Expr[E],
propertyNameExpr: Expr[String], checkerExpr: Expr[SinglePropertyChecker],
propertyValueExpr: Expr[R => Boolean] propertyNameExpr: Expr[String],
)(using Type[T], Type[R])(using Quotes): Expr[Unit] = propertyValueExpr: Expr[Condition[R]]
)(using Type[E], Type[R])(using Quotes): Expr[Unit] =
import quotes.reflect.* import quotes.reflect.*
val subjectTypeRepr = TypeRepr.of[T] val subjectTypeRepr = TypeRepr.of[E]
val optionalCandidateSymbol = val optionalCandidateSymbol =
findCandidateSymbol[T, R](propertyNameExpr.valueOrAbort) findCandidateSymbol[E, R](propertyNameExpr.valueOrAbort)
if optionalCandidateSymbol.isEmpty then if optionalCandidateSymbol.isEmpty then
report.errorAndAbort( report.errorAndAbort(
@ -156,36 +225,63 @@ object GenerativeFluentAssertionsDSL:
) )
else else
val candidateMethod = optionalCandidateSymbol.get val candidateMethod = optionalCandidateSymbol.get
/*
* This version is slightly different than the one presented during val assertion: Expr[Unit] = '{
* the lecture. In this case we also generate a proper message for
* the assertion violation, so we need to declare and reuse a local
* variable, otherwise the same expression with the type provider needs
* to be duplicated and inserted multiple times. So we take the subjectExpr,
* which contains the type provider and selects the subject, and we
* save it to a local val 'subject'. Then we use the low-level AST
* API to retrieve the result:
* first we take '(subject), which is the expression corresponding
* to the subject variable, and we take the low-level term version;
* then, we construct the Select AST which corresponds to the method
* invocation;
* finally, we convert all to an expression back again, which can be
* spliced in the quote.
*/
val assertion = '{
val subject = $subjectExpr val subject = $subjectExpr
val tester = $propertyValueExpr val checker = $checkerExpr
val condition = $propertyValueExpr
lazy val value = ${ Select(('subject).asTerm, candidateMethod).asExprOf[R] } lazy val value = ${ Select(('subject).asTerm, candidateMethod).asExprOf[R] }
assert( assert(
tester.apply(value), checker.check(value, condition),
s"assertion failed for ${subject}.${$propertyNameExpr}" s"$subject.${$propertyNameExpr} ${checker.failedMessage} ${condition.failedMessage} but ${value}"
) )
} }
report.info(assertion.show, subjectExpr.asTerm.pos) report.info(assertion.show, subjectExpr.asTerm.pos)
assertion assertion
end if end if
end generateShouldHaveAssertion end generateShouldHaveAssertion
private def generateShouldContainAssertion[E, R](
subjectExpr: Expr[Iterable[E]],
checkerExpr: Expr[IterablePropertyChecker],
propertyNameExpr: Expr[String],
propertyValueExpr: Expr[Condition[R]]
)(using Type[E], Type[R])(using Quotes): Expr[Unit] =
import quotes.reflect.*
val subjectTypeRepr = TypeRepr.of[E]
val optionalCandidateSymbol =
findCandidateSymbol[E, R](propertyNameExpr.valueOrAbort)
if optionalCandidateSymbol.isEmpty then
report.errorAndAbort(
s"Assertion Error: No field or arity-0 method with name ${propertyNameExpr.valueOrAbort} on type ${subjectTypeRepr.show}",
propertyNameExpr
)
else
val candidateMethod = optionalCandidateSymbol.get
val assertion: Expr[Unit] = '{
val subject = $subjectExpr
val checker = $checkerExpr
val condition = $propertyValueExpr
lazy val values = subject.map(element => ${ Select(('element).asTerm, candidateMethod).asExprOf[R] })
assert(
checker.check(values, condition),
s"$subject ${checker.failedMessage} ${$propertyNameExpr} ${condition.failedMessage}"
)
}
report.info(assertion.show, subjectExpr.asTerm.pos)
assertion
end if
end generateShouldContainAssertion
/** Looks for candidate symbols with a given set of possible names on a type. /** Looks for candidate symbols with a given set of possible names on a type.
* *
* Improved version w.r.t. the one presented during the lecture. * Improved version w.r.t. the one presented during the lecture.

View file

@ -1,8 +1,10 @@
package ch.usi.si.msde.edsl.lecture10 package ch.usi.si.msde.edsl.lecture10
import scala.language.dynamics import scala.language.dynamics
import scala.quoted.Expr
sealed trait AssertionProperty sealed trait AssertionProperty
case class NegatedAssertionProperty(prop: AssertionProperty) extends AssertionProperty
/** Represents (but with no implementation) the result of a call to /** Represents (but with no implementation) the result of a call to
* be.selectDynamic(String), used to specify the property. The semantics of * be.selectDynamic(String), used to specify the property. The semantics of
@ -12,48 +14,92 @@ class DynamicShouldBeProperty extends AssertionProperty with Dynamic:
def applyDynamic(fieldName: String)(foo: Any*) = ??? def applyDynamic(fieldName: String)(foo: Any*) = ???
end DynamicShouldBeProperty end DynamicShouldBeProperty
object satisfying: sealed trait SatisfyingNotEquals[T]
def `===`[T](toWhat: T)(using ord: Ordering[T]): T => Boolean =
something => ord.eq(something, toWhat) case class Condition[-T](predicate: T => Boolean, op: String, expected: Any):
def `!==`[T](toWhat: T)(using ord: Ordering[T]): T => Boolean = def test(value: T): Boolean = predicate(value)
something => ord.ne(something, toWhat) def failedMessage: String = s"${op} ${expected}"
def `<`[T](toWhat: T)(using ord: Ordering[T]): T => Boolean =
something => ord.lt(something, toWhat) case object satisfying:
def `>`[T](toWhat: T)(using ord: Ordering[T]): T => Boolean = def `===`[T](toWhat: T)(using ord: Ordering[T]): Condition[T] = Condition(ord.equiv(_, toWhat), "===", toWhat)
something => ord.gt(something, toWhat) def notEquals[T](toWhat: T, ord: Ordering[T]): Condition[T] = Condition(!ord.equiv(_, toWhat), "!==", toWhat)
def `<=`[T](toWhat: T)(using ord: Ordering[T]): T => Boolean = def `<`[T](toWhat: T)(using ord: Ordering[T]): Condition[T] = Condition(ord.lt(_, toWhat), "<", toWhat)
something => ord.lteq(something, toWhat) def `>`[T](toWhat: T)(using ord: Ordering[T]): Condition[T] = Condition(ord.gt(_, toWhat), ">", toWhat)
def `>=`[T](toWhat: T)(using ord: Ordering[T]): T => Boolean = def `<=`[T](toWhat: T)(using ord: Ordering[T]): Condition[T] = Condition(ord.lteq(_, toWhat), "<=", toWhat)
something => ord.gteq(something, toWhat) def `>=`[T](toWhat: T)(using ord: Ordering[T]): Condition[T] = Condition(ord.gteq(_, toWhat), ">=", toWhat)
end satisfying
class BePropertyTypeProvider[T](val subject: T) extends Selectable: class BePropertyTypeProvider[T](val subject: T) extends Selectable:
def selectDynamic(fieldName: String): AssertionProperty = ??? def selectDynamic(fieldName: String): AssertionProperty = ???
def applyDynamic(fieldName: String)(foo: Any*): AssertionProperty = ??? def applyDynamic(fieldName: String)(foo: Any*): AssertionProperty = ???
end BePropertyTypeProvider end BePropertyTypeProvider
/** The class that implements the syntactic aspect of the type provider. It sealed trait PropertyChecker:
* extends Selectable and implements applyDynamic with arity 1, so that any val failedMessage: String
* object of this type can be invoked with an arbitrary method with 1 argument,
* and a specific return type. sealed trait SinglePropertyChecker extends PropertyChecker:
* def check[T](t: T, condition: Condition[T]): Boolean
* Selectable types can be refined with a list of specific methods that are
* available and thus checked by the compiler. The macro in the should method case object SingleChecker extends SinglePropertyChecker:
* generates a refinement of this type according to type T, so that - for any override def check[T](t: T, condition: Condition[T]): Boolean = condition.test(t)
* arity zero method or field in T - say foo: X, the type provider has a override val failedMessage: String = "is not"
* corresponding method def foo(arg: X): AssertionProperty.
*/ case object NegatedChecker extends SinglePropertyChecker:
class HavePropertyTypeProvider[T](val subject: T) extends Selectable: override def check[T](t: T, condition: Condition[T]): Boolean = !condition.test(t)
override val failedMessage: String = "is"
sealed trait IterablePropertyChecker extends PropertyChecker:
def check[T](t: Iterable[T], condition: Condition[T]): Boolean
case object AllChecker extends IterablePropertyChecker:
override def check[T](t: Iterable[T], condition: Condition[T]): Boolean = t.forall(e => condition.test(e))
override val failedMessage: String = "has elements not satisfying"
case object SomeChecker extends IterablePropertyChecker:
override def check[T](t: Iterable[T], condition: Condition[T]): Boolean = t.exists(e => condition.test(e))
override val failedMessage: String = "has no elements satisfying"
case object NoChecker extends IterablePropertyChecker:
override def check[T](t: Iterable[T], condition: Condition[T]): Boolean = t.forall(e => !condition.test(e))
override val failedMessage: String = "has elements satisfying"
class HavePropertyTypeProvider[T](val subject: T, val checker: SinglePropertyChecker) extends Selectable:
def applyDynamic[U](fieldName: String)(arg: satisfying.type): SatisfyingNotEquals[U] = ???
def applyDynamic(fieldName: String)(arg: Any): AssertionProperty = ??? def applyDynamic(fieldName: String)(arg: Any): AssertionProperty = ???
end HavePropertyTypeProvider end HavePropertyTypeProvider
class ContainsPropertyTypeProvider[T](val subject: Iterable[T], val checker: IterablePropertyChecker) 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). /** A trait for each type of assertion (be or have).
*/ */
sealed trait AssertionDSLVerb sealed trait AssertionDSLVerb
case object be extends AssertionDSLVerb with Dynamic: case object be extends AssertionDSLVerb with Dynamic:
def selectDynamic(fieldName: String): AssertionProperty = ??? def selectDynamic(fieldName: String): AssertionProperty = ???
case object be_ extends AssertionDSLVerb case object be_ extends AssertionDSLVerb
case object have extends AssertionDSLVerb
case object have extends AssertionDSLVerb:
inline def `unary_!`: have.type = ???
case object contain extends AssertionDSLVerb
case object `with` extends AssertionDSLVerb
case object `!!` extends AssertionDSLVerb
case class ContainsVerb[T](subject: Iterable[T])
extension [T](inline containsVerb: ContainsVerb[T])
transparent inline def allElements(inline verb: `with`.type) = ${
ShouldContainTypeProvider.generateShouldContainTypeProvider('containsVerb, '{ AllChecker })
}
transparent inline def someElements(inline verb: `with`.type) = ${
ShouldContainTypeProvider.generateShouldContainTypeProvider('containsVerb, '{ SomeChecker })
}
transparent inline def noElements(inline verb: `with`.type) = ${
ShouldContainTypeProvider.generateShouldContainTypeProvider('containsVerb, '{ NoChecker })
}
/** Implicit class to add 'should' assertions to a generic type T. /** Implicit class to add 'should' assertions to a generic type T.
* *
@ -61,17 +107,15 @@ case object have extends AssertionDSLVerb
* the object to which perform the assertion. * the object to which perform the assertion.
*/ */
extension [T](subject: T) 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) = ??? 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<n>' 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) = ${ transparent inline def should(inline verbWord: be_.type) = ${
ShouldBeTypeProvider.generateShouldBeTypeProvider[T]('subject) ShouldBeTypeProvider.generateShouldBeTypeProvider[T]('subject)
} }
@ -85,11 +129,21 @@ extension [T](subject: T)
* *
* The method semantics is then provided by the assertions macro. * The method semantics is then provided by the assertions macro.
* *
* @param verbWord * @param verb
* The have word. * The have word.
* @return * @return
* a type provider for properties of type T. * a type provider for properties of type T.
*/ */
transparent inline def should(inline verbWord: have.type) = ${ transparent inline def should(inline verb: have.type) = ${
ShouldHaveTypeProvider.generateShouldHaveTypeProvider[T]('subject) ShouldHaveTypeProvider.generateShouldHaveTypeProvider[T]('subject, 'verb)
} }
// 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)
}

View file

@ -2,12 +2,12 @@ package ch.usi.si.msde.edsl.lecture10
// import scala.language.postfixOps // import scala.language.postfixOps
import squants.mass.MassConversions.MassConversions import ch.usi.si.msde.edsl.lecture10.GenerativeFluentAssertionsDSL.*
import squants.market.MoneyConversions.MoneyConversions
import GenerativeFluentAssertionsDSL.*
import squants.mass.Mass
import squants.market.Money import squants.market.Money
import squants.market.MoneyConversions.MoneyConversions
import squants.mass.Mass
import squants.mass.MassConversions.MassConversions
import scala.collection.immutable._
extension (value: Double) def i = Complex(0, value) extension (value: Double) def i = Complex(0, value)
@ -17,6 +17,7 @@ case class Complex(re: Double, im: Double):
case class Person(firstName: String, lastName: String, age: Int): case class Person(firstName: String, lastName: String, age: Int):
def adult: Boolean = age >= 18 def adult: Boolean = age >= 18
def minor: Boolean = !adult
case class Box(label: String, weight: Mass, price: Money) case class Box(label: String, weight: Mass, price: Money)
@ -25,37 +26,43 @@ case class Box(label: String, weight: Mass, price: Money)
val box = Box("aBox", 30.kg, 10.CHF) val box = Box("aBox", 30.kg, 10.CHF)
assertions: assertions:
// { // Exercise 1 solution:
// val p: BePropertyTypeProvider[Person] = new BePropertyTypeProvider[Person](person) // It is possible to implement a DSL for 'should be' assertions that uses a type provider like the 'should have'
// p.asInstanceOf[BePropertyTypeProvider[Person] { // assertion. However, a slight change to the syntax is needed in order to maintain the 'subject [method value]+'
// def adult: AssertionProperty // pattern that DSL statements should maintain in order to be parsed correctly by the Scala parser. To do this,
// }].adult // we define an extra token named '!!' as a case object that should be appended at the end of the assertion. This
// } // object will serve as a dummy parameter to the method called on the type provider to allow the assertion
// statement to be parsed correctly.
/* be.property assertions */ // (note that the verb is 'be_' instead of 'be' to avoid a clash with the original implementation)
// assert(person.adult, ...) person should be_ adult !!
person should be.adult
// assert(List().isEmpty, ...)
List() should be.empty
// assert(List(1,2,3).nonEmpty)
List(1, 2, 3) should be.nonEmpty
// Exercise 2
List(1) should have head satisfying === 1
box should have weight satisfying === 30.kg
List() should have size satisfying >= 0
List(3) should have size satisfying !== 0
List(50, 2, 3) should have head satisfying < 100
person should have age satisfying >= 10 person should have age satisfying >= 10
// New syntax for `be` with type provider // Exercise 3
// either adult is a method with a dummy Unit parameter box should !(!have) weight satisfying <= 300.0.kg
person should be_ adult() person should !(!be.adult)
// or adult is a property but braces are needed to resolve the type provider person should !be.minor
(person should be_).adult
/* should have assertions */ // Exercise 4
// // assert(List(1).head == 1, ...)
// List(1) should have head 1 // we wrap the strings in StringOps instances in order to resolve the scala method "size". This would otherwise
// // assert(box.weight == 30.kg, ...) // not happen automatically as the string types would resolve to "java.lang.String" thus not allowing the type
// box should have weight 30.kg // provider to resolve the "size" method. This is due to the String -> StringOps conversion being an 'implicit def'
// // assert(person.age == 0x29, ...) // that does not trigger here as we do not access the 'size' method directly, but instead we resolve it via reflection.
// person should have age 0x29 List(
// List(2, 3) should have tail List(3) new StringOps("bar"),
// 3.i + 1 should have re 1.0 new StringOps("foo"),
new StringOps(""),
new StringOps("alice")
) should contain someElements `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 // this assertion fails
end assertionsExample end assertionsExample

View file

@ -16,8 +16,8 @@ object ShouldBeTypeProvider:
): TypeRepr = ): TypeRepr =
// refine both as unary method with unit parameter and as property // refine both as unary method with unit parameter and as property
// and let client code choose // and let client code choose
val methodType = MethodType(List("unit"))( val methodType = MethodType(List("end"))(
_ => List(TypeRepr.of[Unit]), _ => List(TypeRepr.of[`!!`.type]),
_ => TypeRepr.of[AssertionProperty] _ => TypeRepr.of[AssertionProperty]
) )
Refinement( Refinement(
@ -53,8 +53,6 @@ object ShouldBeTypeProvider:
members members
) )
// println(refinedType)
// This is the way to extract a (reflection) type to a // This is the way to extract a (reflection) type to a
// type that can be used in some quoted code. // type that can be used in some quoted code.
// it's the equivalent of .asExpr for expressions, // it's the equivalent of .asExpr for expressions,
@ -66,7 +64,6 @@ object ShouldBeTypeProvider:
val p = BePropertyTypeProvider[T]($subject) val p = BePropertyTypeProvider[T]($subject)
p.asInstanceOf[tpe] p.asInstanceOf[tpe]
} }
println(res.show)
res res
end ShouldBeTypeProvider end ShouldBeTypeProvider

View file

@ -0,0 +1,92 @@
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](containsExpr: Expr[ContainsVerb[T]], checker: Expr[IterablePropertyChecker])
(using Type[T], Quotes): Expr[Any] =
import quotes.reflect.*
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
// some reason
def typeConstructor[U](using Type[U]): TypeRepr =
AppliedType.unapply(TypeRepr.of[U].asInstanceOf[AppliedType])._1
val subjectTypeRepr = TypeRepr.of[T].widen
/** 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
)
// 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, $checker).asInstanceOf[tpe] }
end ShouldContainTypeProvider

View file

@ -4,14 +4,30 @@ import scala.quoted.*
object ShouldHaveTypeProvider: object ShouldHaveTypeProvider:
/** The type provider for the should have assertion, which generates a refined /** The type provider for the should have assertion, which generates a refined
* structural type for type T. * structural type for type T.
*/ */
def generateShouldHaveTypeProvider[T](subject: Expr[T])(using Type[T])(using def generateShouldHaveTypeProvider[T](subject: Expr[T], verb: Expr[have.type])(using Type[T])(using
Quotes Quotes
): Expr[Any] = ): Expr[Any] =
import quotes.reflect.* import quotes.reflect.*
def isNegated(verb: Expr[have.type]): Boolean =
verb match
case '{ ($inner: have.type).`unary_!` } => !isNegated(inner)
case '{ have: have.type } => false
val negated = isNegated(verb)
// 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] val subjectTypeRepr = TypeRepr.of[T]
/** Given a refinable current type, and the symbol of a arity-0 method or a /** Given a refinable current type, and the symbol of a arity-0 method or a
@ -25,30 +41,21 @@ object ShouldHaveTypeProvider:
// The type of the field, or the return type of the method. // The type of the field, or the return type of the method.
val fieldTypeRepr = subjectTypeRepr.memberType(symbol).widen 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 methodType = MethodType(List("arg"))( val methodType = MethodType(List("arg"))(
_ => List(appliedType), _ => List(AppliedType(typeConstructor[Condition[?]], List(fieldTypeRepr))),
_ => TypeRepr.of[AssertionProperty] _ => TypeRepr.of[AssertionProperty]
).widen )
// returns the refinement of currentTypeRepr -
// symbol.name is the name of the method, val chainedMethodType = MethodType(List("arg"))(
// methodType is its type. _ => List(TypeRepr.of[satisfying.type]),
val refinement = Refinement(currentTypeRepr, symbol.name, methodType) _ => AppliedType(typeConstructor[SatisfyingNotEquals[?]], List(fieldTypeRepr))
refinement )
Refinement(
Refinement(currentTypeRepr, symbol.name, chainedMethodType),
symbol.name,
methodType
)
/** Refines a type according to a list of fields or methods of arity 0. /** Refines a type according to a list of fields or methods of arity 0.
*/ */
@ -79,7 +86,7 @@ object ShouldHaveTypeProvider:
fields ++ arityZeroMethods fields ++ arityZeroMethods
) )
println(refinedType.show) val checker = if (negated) '{ NegatedChecker } else '{ SingleChecker }
// This is the way to extract a (reflection) type to a // This is the way to extract a (reflection) type to a
// type that can be used in some quoted code. // type that can be used in some quoted code.
@ -88,10 +95,6 @@ object ShouldHaveTypeProvider:
// exact type is unknown until compilation time. // exact type is unknown until compilation time.
refinedType.asType match refinedType.asType match
case '[tpe] => // tpe is the exact refined type case '[tpe] => // tpe is the exact refined type
val res = '{ '{ HavePropertyTypeProvider[T]($subject, $checker).asInstanceOf[tpe] }
val p = HavePropertyTypeProvider[T]($subject)
p.asInstanceOf[tpe]
}
res
end ShouldHaveTypeProvider end ShouldHaveTypeProvider