Compare commits
1 commit
Author | SHA1 | Date | |
---|---|---|---|
9b1765f8bf |
8 changed files with 138 additions and 417 deletions
24
README.md
24
README.md
|
@ -1,23 +1,3 @@
|
|||
# Assignment 03
|
||||
## 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)
|
||||
Code for Lecture 12.
|
|
@ -4,7 +4,7 @@ lazy val root = project
|
|||
.in(file("."))
|
||||
.settings(
|
||||
organization := "ch.usi.si.msde.edsl",
|
||||
name := "assignment-04a",
|
||||
name := "lecture-10",
|
||||
version := "2024.01",
|
||||
scalaVersion := scala3Version,
|
||||
libraryDependencies += "org.typelevel" %% "squants" % "1.8.3",
|
||||
|
|
|
@ -8,52 +8,6 @@ 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[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
|
||||
* AssertionProperty expressions and rewrites them into actual assertions.
|
||||
*/
|
||||
|
@ -61,19 +15,6 @@ object GenerativeFluentAssertionsDSL:
|
|||
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
|
||||
* then separately rewritten into the generateAssertion method. The method
|
||||
* returns a compiler error if the expression is not an AssertionProperty.
|
||||
|
@ -119,46 +60,40 @@ object GenerativeFluentAssertionsDSL:
|
|||
|
||||
val generatedAssertion = assertionExpr match
|
||||
// be assertion. Check the explicit selectDynamic to extract the property name
|
||||
case '{ (${ subjectExpr }: subjectType) should ($assertionProperty: AssertionProperty) } =>
|
||||
val (propertyNameExpr, negated) = unwrapShouldBeNegatedAssertion(assertionProperty)
|
||||
generateShouldBeAssertion[subjectType](subjectExpr, propertyNameExpr, negated)
|
||||
case '{
|
||||
(${ subjectExpr }: subjectType) should be.selectDynamic(
|
||||
$propertyNameExpr
|
||||
)
|
||||
} =>
|
||||
generateShouldBeAssertion[subjectType](subjectExpr, propertyNameExpr)
|
||||
// The more complicated structure of property type provider construction in
|
||||
// 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)($valueExpr: Condition[valueType])
|
||||
.applyDynamic($propertyNameExpr)($valueExpr: valueType => Boolean)
|
||||
.$asInstanceOf$[AssertionProperty]
|
||||
} =>
|
||||
val subjectExpr = '{ $typeProvider.subject }
|
||||
val checkerExpr = '{ $typeProvider.checker }
|
||||
generateShouldHaveAssertion[subjectType, valueType](
|
||||
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,
|
||||
valueExpr
|
||||
)
|
||||
case '{
|
||||
(${ typeProvider }: BePropertyTypeProvider[subjectType])
|
||||
.applyDynamic($propertyNameExpr)($end)
|
||||
.applyDynamic($propertyNameExpr)($unit)
|
||||
.$asInstanceOf$[AssertionProperty]
|
||||
} =>
|
||||
'{} // placeholder
|
||||
case a =>
|
||||
case '{
|
||||
(${ typeProvider }: BePropertyTypeProvider[subjectType])
|
||||
.selectDynamic($propertyNameExpr)
|
||||
.$asInstanceOf$[AssertionProperty]
|
||||
} =>
|
||||
'{} // placeholder
|
||||
case _ =>
|
||||
report.errorAndAbort(
|
||||
"Invalid expression, must be a 'should be' or 'should have' assertion. ",
|
||||
assertionExpr
|
||||
|
@ -169,8 +104,7 @@ object GenerativeFluentAssertionsDSL:
|
|||
|
||||
private def generateShouldBeAssertion[T](
|
||||
subject: Expr[T],
|
||||
propertyNameExpr: Expr[String],
|
||||
negated: Boolean
|
||||
propertyNameExpr: Expr[String]
|
||||
)(using Type[T])(using Quotes): Expr[Unit] =
|
||||
import quotes.reflect.*
|
||||
|
||||
|
@ -193,30 +127,27 @@ object GenerativeFluentAssertionsDSL:
|
|||
// Constructs a 'select' tree, that is, a selection to the field or method
|
||||
// of the subject.
|
||||
val call = Select(subject.asTerm, validSymbol).asExprOf[Boolean]
|
||||
val expected = '{ !${Expr(negated)} }
|
||||
|
||||
val assertion = '{
|
||||
assert(
|
||||
$call == $expected,
|
||||
s"${$subject} was${if ($expected) " not" else ""} ${$propertyNameExpr} (${$validSymbolNameExpr} is ${if ($expected) "false" else "true"})"
|
||||
$call,
|
||||
s"${$subject} was not ${$propertyNameExpr} (${$validSymbolNameExpr} is false)"
|
||||
)
|
||||
}
|
||||
assertion
|
||||
end if
|
||||
end generateShouldBeAssertion
|
||||
|
||||
private def generateShouldHaveAssertion[E, R](
|
||||
subjectExpr: Expr[E],
|
||||
checkerExpr: Expr[SinglePropertyChecker],
|
||||
private def generateShouldHaveAssertion[T, R](
|
||||
subjectExpr: Expr[T],
|
||||
propertyNameExpr: Expr[String],
|
||||
propertyValueExpr: Expr[Condition[R]]
|
||||
)(using Type[E], Type[R])(using Quotes): Expr[Unit] =
|
||||
propertyValueExpr: Expr[R => Boolean]
|
||||
)(using Type[T], Type[R])(using Quotes): Expr[Unit] =
|
||||
import quotes.reflect.*
|
||||
|
||||
val subjectTypeRepr = TypeRepr.of[E]
|
||||
|
||||
val subjectTypeRepr = TypeRepr.of[T]
|
||||
val optionalCandidateSymbol =
|
||||
findCandidateSymbol[E, R](propertyNameExpr.valueOrAbort)
|
||||
findCandidateSymbol[T, R](propertyNameExpr.valueOrAbort)
|
||||
|
||||
if optionalCandidateSymbol.isEmpty then
|
||||
report.errorAndAbort(
|
||||
|
@ -225,63 +156,36 @@ object GenerativeFluentAssertionsDSL:
|
|||
)
|
||||
else
|
||||
val candidateMethod = optionalCandidateSymbol.get
|
||||
|
||||
val assertion: Expr[Unit] = '{
|
||||
/*
|
||||
* This version is slightly different than the one presented during
|
||||
* 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 checker = $checkerExpr
|
||||
val condition = $propertyValueExpr
|
||||
val tester = $propertyValueExpr
|
||||
lazy val value = ${ Select(('subject).asTerm, candidateMethod).asExprOf[R] }
|
||||
|
||||
assert(
|
||||
checker.check(value, condition),
|
||||
s"$subject.${$propertyNameExpr} ${checker.failedMessage} ${condition.failedMessage} but ${value}"
|
||||
tester.apply(value),
|
||||
s"assertion failed for ${subject}.${$propertyNameExpr}"
|
||||
)
|
||||
}
|
||||
|
||||
report.info(assertion.show, subjectExpr.asTerm.pos)
|
||||
assertion
|
||||
end if
|
||||
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.
|
||||
*
|
||||
* Improved version w.r.t. the one presented during the lecture.
|
||||
|
|
|
@ -1,10 +1,8 @@
|
|||
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
|
||||
|
||||
/** Represents (but with no implementation) the result of a call to
|
||||
* be.selectDynamic(String), used to specify the property. The semantics of
|
||||
|
@ -14,92 +12,50 @@ class DynamicShouldBeProperty extends AssertionProperty with Dynamic:
|
|||
def applyDynamic(fieldName: String)(foo: Any*) = ???
|
||||
end DynamicShouldBeProperty
|
||||
|
||||
sealed trait SatisfyingNotEquals[T]
|
||||
case object satisfying
|
||||
|
||||
case class Condition[-T](predicate: T => Boolean, op: String, expected: Any):
|
||||
def test(value: T): Boolean = predicate(value)
|
||||
def failedMessage: String = s"${op} ${expected}"
|
||||
case class AssertionCandidate[T](value: T, subject: String, propertyName: String):
|
||||
private def comparison(other: T, predicate: (T, T) => Boolean, predicateOp: String): Unit =
|
||||
assert(
|
||||
predicate.apply(value, other),
|
||||
s"${subject} did not have ${propertyName} ${predicateOp} ${other}, but it was equal to ${value}"
|
||||
)
|
||||
|
||||
case object satisfying:
|
||||
def `===`[T](toWhat: T)(using ord: Ordering[T]): Condition[T] = Condition(ord.equiv(_, toWhat), "===", toWhat)
|
||||
def notEquals[T](toWhat: T, ord: Ordering[T]): Condition[T] = Condition(!ord.equiv(_, toWhat), "!==", toWhat)
|
||||
def `<`[T](toWhat: T)(using ord: Ordering[T]): Condition[T] = Condition(ord.lt(_, toWhat), "<", toWhat)
|
||||
def `>`[T](toWhat: T)(using ord: Ordering[T]): Condition[T] = Condition(ord.gt(_, toWhat), ">", toWhat)
|
||||
def `<=`[T](toWhat: T)(using ord: Ordering[T]): Condition[T] = Condition(ord.lteq(_, toWhat), "<=", toWhat)
|
||||
def `>=`[T](toWhat: T)(using ord: Ordering[T]): Condition[T] = Condition(ord.gteq(_, toWhat), ">=", toWhat)
|
||||
def `===`(other: T)(using ord: Ordering[T]): Unit = comparison(other, ord.eq(_, _), "==")
|
||||
def `!==`(other: T)(using ord: Ordering[T]): Unit = comparison(other, ord.ne(_, _), "!=")
|
||||
def `<=`(other: T)(using ord: Ordering[T]): Unit = comparison(other, ord.lteq, "<=")
|
||||
def `>=`(other: T)(using ord: Ordering[T]): Unit = comparison(other, ord.gteq, ">=")
|
||||
def `<`(other: T)(using ord: Ordering[T]): Unit = comparison(other, ord.lt, "<")
|
||||
def `>`(other: T)(using ord: Ordering[T]): Unit = comparison(other, ord.gt, ">")
|
||||
end AssertionCandidate
|
||||
|
||||
class BePropertyTypeProvider[T](val subject: T) extends Selectable:
|
||||
def selectDynamic(fieldName: String): AssertionProperty = ???
|
||||
def applyDynamic(fieldName: String)(foo: Any*): AssertionProperty = ???
|
||||
end BePropertyTypeProvider
|
||||
|
||||
sealed trait PropertyChecker:
|
||||
val failedMessage: String
|
||||
|
||||
sealed trait SinglePropertyChecker extends PropertyChecker:
|
||||
def check[T](t: T, condition: Condition[T]): Boolean
|
||||
|
||||
case object SingleChecker extends SinglePropertyChecker:
|
||||
override def check[T](t: T, condition: Condition[T]): Boolean = condition.test(t)
|
||||
override val failedMessage: String = "is not"
|
||||
|
||||
case object NegatedChecker extends SinglePropertyChecker:
|
||||
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 = ???
|
||||
/** The class that implements the syntactic aspect of the type provider. It
|
||||
* extends Selectable and implements applyDynamic with arity 1, so that any
|
||||
* object of this type can be invoked with an arbitrary method with 1 argument,
|
||||
* and a specific return type.
|
||||
*
|
||||
* 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
|
||||
* generates a refinement of this type according to type T, so that - for any
|
||||
* arity zero method or field in T - say foo: X, the type provider has a
|
||||
* corresponding method def foo(arg: X): AssertionProperty.
|
||||
*/
|
||||
class HavePropertyTypeProvider[T](val subject: T) extends Selectable:
|
||||
def applyDynamic(fieldName: String)(arg: Any): AssertionCandidate[T] = ???
|
||||
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).
|
||||
*/
|
||||
sealed trait AssertionDSLVerb
|
||||
case object be extends AssertionDSLVerb with Dynamic:
|
||||
def selectDynamic(fieldName: String): AssertionProperty = ???
|
||||
case object be_ 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 })
|
||||
}
|
||||
case object have extends AssertionDSLVerb
|
||||
|
||||
/** Implicit class to add 'should' assertions to a generic type T.
|
||||
*
|
||||
|
@ -107,15 +63,17 @@ extension [T](inline containsVerb: ContainsVerb[T])
|
|||
* 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) = ???
|
||||
|
||||
// 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) = ${
|
||||
ShouldBeTypeProvider.generateShouldBeTypeProvider[T]('subject)
|
||||
}
|
||||
|
@ -129,21 +87,11 @@ extension [T](inline subject: T)
|
|||
*
|
||||
* The method semantics is then provided by the assertions macro.
|
||||
*
|
||||
* @param verb
|
||||
* @param verbWord
|
||||
* The have word.
|
||||
* @return
|
||||
* a type provider for properties of type T.
|
||||
*/
|
||||
transparent inline def should(inline verb: have.type) = ${
|
||||
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)
|
||||
transparent inline def should(inline verbWord: have.type) = ${
|
||||
ShouldHaveTypeProvider.generateShouldHaveTypeProvider[T]('subject)
|
||||
}
|
|
@ -2,12 +2,12 @@ package ch.usi.si.msde.edsl.lecture10
|
|||
|
||||
// import scala.language.postfixOps
|
||||
|
||||
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
|
||||
import scala.collection.immutable._
|
||||
import squants.market.MoneyConversions.MoneyConversions
|
||||
|
||||
import GenerativeFluentAssertionsDSL.*
|
||||
import squants.mass.Mass
|
||||
import squants.market.Money
|
||||
|
||||
extension (value: Double) def i = Complex(0, value)
|
||||
|
||||
|
@ -17,7 +17,6 @@ case class Complex(re: Double, im: Double):
|
|||
|
||||
case class Person(firstName: String, lastName: String, age: Int):
|
||||
def adult: Boolean = age >= 18
|
||||
def minor: Boolean = !adult
|
||||
|
||||
case class Box(label: String, weight: Mass, price: Money)
|
||||
|
||||
|
@ -26,43 +25,38 @@ case class Box(label: String, weight: Mass, price: Money)
|
|||
val box = Box("aBox", 30.kg, 10.CHF)
|
||||
|
||||
assertions:
|
||||
// Exercise 1 solution:
|
||||
// It is possible to implement a DSL for 'should be' assertions that uses a type provider like the 'should have'
|
||||
// assertion. However, a slight change to the syntax is needed in order to maintain the 'subject [method value]+'
|
||||
// pattern that DSL statements should maintain in order to be parsed correctly by the Scala parser. To do this,
|
||||
// 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.
|
||||
// (note that the verb is 'be_' instead of 'be' to avoid a clash with the original implementation)
|
||||
person should be_ adult !!
|
||||
/* be.property assertions */
|
||||
// assert(person.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
|
||||
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 >= 10.kg
|
||||
person should have age satisfying >= 10
|
||||
|
||||
// Exercise 3
|
||||
box should !(!have) weight satisfying <= 300.0.kg
|
||||
person should !(!be.adult)
|
||||
person should !be.minor
|
||||
// New syntax for `be` with type provider
|
||||
// either adult is a method with a dummy Unit parameter
|
||||
person should be_ adult()
|
||||
// or adult is a property but braces are needed to resolve the type provider
|
||||
(person should be_).adult
|
||||
|
||||
// Exercise 4
|
||||
|
||||
// we wrap the strings in StringOps instances in order to resolve the scala method "size". This would otherwise
|
||||
// not happen automatically as the string types would resolve to "java.lang.String" thus not allowing the type
|
||||
// provider to resolve the "size" method. This is due to the String -> StringOps conversion being an 'implicit def'
|
||||
// that does not trigger here as we do not access the 'size' method directly, but instead we resolve it via reflection.
|
||||
List(
|
||||
new StringOps("bar"),
|
||||
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
|
||||
/* should have assertions */
|
||||
// // assert(List(1).head == 1, ...)
|
||||
// List(1) should have head 1
|
||||
// // assert(box.weight == 30.kg, ...)
|
||||
// box should have weight 30.kg
|
||||
// // assert(person.age == 0x29, ...)
|
||||
// person should have age 0x29
|
||||
// List(2, 3) should have tail List(3)
|
||||
// 3.i + 1 should have re 1.0
|
||||
|
||||
end assertionsExample
|
||||
|
|
|
@ -16,8 +16,8 @@ object ShouldBeTypeProvider:
|
|||
): TypeRepr =
|
||||
// refine both as unary method with unit parameter and as property
|
||||
// and let client code choose
|
||||
val methodType = MethodType(List("end"))(
|
||||
_ => List(TypeRepr.of[`!!`.type]),
|
||||
val methodType = MethodType(List("unit"))(
|
||||
_ => List(TypeRepr.of[Unit]),
|
||||
_ => TypeRepr.of[AssertionProperty]
|
||||
)
|
||||
Refinement(
|
||||
|
@ -53,6 +53,8 @@ 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,
|
||||
|
|
|
@ -1,92 +0,0 @@
|
|||
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
|
|
@ -4,30 +4,14 @@ import scala.quoted.*
|
|||
|
||||
object ShouldHaveTypeProvider:
|
||||
|
||||
|
||||
|
||||
/** The type provider for the should have assertion, which generates a refined
|
||||
* structural type for type T.
|
||||
*/
|
||||
def generateShouldHaveTypeProvider[T](subject: Expr[T], verb: Expr[have.type])(using Type[T])(using
|
||||
def generateShouldHaveTypeProvider[T](subject: Expr[T])(using Type[T])(using
|
||||
Quotes
|
||||
): Expr[Any] =
|
||||
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]
|
||||
|
||||
/** Given a refinable current type, and the symbol of a arity-0 method or a
|
||||
|
@ -41,21 +25,18 @@ object ShouldHaveTypeProvider:
|
|||
// 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]
|
||||
)
|
||||
// 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 acTypeCtor = AppliedType.unapply(TypeRepr.of[AssertionCandidate[Unit]].asInstanceOf[AppliedType])._1
|
||||
val assertionCandidateType = AppliedType(acTypeCtor, List(fieldTypeRepr))
|
||||
|
||||
val chainedMethodType = MethodType(List("arg"))(
|
||||
val methodType = MethodType(List("satisfying"))(
|
||||
_ => List(TypeRepr.of[satisfying.type]),
|
||||
_ => AppliedType(typeConstructor[SatisfyingNotEquals[?]], List(fieldTypeRepr))
|
||||
)
|
||||
|
||||
Refinement(
|
||||
Refinement(currentTypeRepr, symbol.name, chainedMethodType),
|
||||
symbol.name,
|
||||
methodType
|
||||
_ => assertionCandidateType
|
||||
)
|
||||
Refinement(currentTypeRepr, symbol.name, methodType)
|
||||
|
||||
/** Refines a type according to a list of fields or methods of arity 0.
|
||||
*/
|
||||
|
@ -86,7 +67,7 @@ object ShouldHaveTypeProvider:
|
|||
fields ++ arityZeroMethods
|
||||
)
|
||||
|
||||
val checker = if (negated) '{ NegatedChecker } else '{ SingleChecker }
|
||||
println(refinedType.show)
|
||||
|
||||
// This is the way to extract a (reflection) type to a
|
||||
// type that can be used in some quoted code.
|
||||
|
@ -95,6 +76,10 @@ object ShouldHaveTypeProvider:
|
|||
// exact type is unknown until compilation time.
|
||||
refinedType.asType match
|
||||
case '[tpe] => // tpe is the exact refined type
|
||||
'{ HavePropertyTypeProvider[T]($subject, $checker).asInstanceOf[tpe] }
|
||||
val res = '{
|
||||
val p = HavePropertyTypeProvider[T]($subject)
|
||||
p.asInstanceOf[tpe]
|
||||
}
|
||||
res
|
||||
|
||||
end ShouldHaveTypeProvider
|
||||
|
|
Loading…
Reference in a new issue