Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add aggregate method definitions for ADT support #439

Merged
merged 6 commits into from
Sep 22, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,10 @@ trait Types {

sealed trait TypeDefinition extends Definition

sealed trait AggregateDefinition extends TypeDefinition {
def typeEx: TypeExpression
}

/** Base trait of an expression that defines a type
*/
sealed trait TypeExpression extends RiddlValue {
Expand Down Expand Up @@ -245,6 +249,7 @@ trait Types {
brief: Option[LiteralString] = Option.empty[LiteralString],
description: Option[Description] = None
) extends LeafDefinition
with AggregateDefinition
with AlwaysEmpty
with TypeDefinition
with SagaDefinition
Expand All @@ -255,27 +260,74 @@ trait Types {
final val kind: String = "Field"
}

/** An argument to a method */
case class MethodArgument(
loc: At,
key: String,
value: TypeExpression
) extends RiddlNode {

/** Format the node to a string */
def format: String = s"$key: ${value.format}"

}

/** A leaf definition that is a callable method (function) of an aggregation
* type expressions. Methods associate an identifier with a computed type
* expression.
*
* @param loc
* The location of the field definition
* @param id
* The name of the field
* @param args
* The type of the field
* @param brief
* A brief description (one sentence) for use in documentation
* @param description
* An optional description of the field.
*/
case class Method(
loc: At,
id: Identifier,
args: Seq[MethodArgument] = Seq.empty[MethodArgument],
typeEx: TypeExpression,
brief: Option[LiteralString] = Option.empty[LiteralString],
description: Option[Description] = None
) extends LeafDefinition
with AggregateDefinition
with AlwaysEmpty
with TypeDefinition
with SagaDefinition
with StateDefinition
with FunctionDefinition
with ProjectorDefinition {
override def format: String = s"${id.format}(${args.map(_.format).mkString(", ")}): ${typeEx.format}"
final val kind: String = "Field"
}

/** A type expression that contains an aggregation of fields
*
* This is used as the base trait of Aggregations and Messages
*/
trait AggregateTypeExpression extends TypeExpression with Container[Field] {
def fields: Seq[Field]
final lazy val contents: Seq[Field] = fields
override def format: String = s"{ ${fields.map(_.format).mkString(", ")} }"
trait AggregateTypeExpression extends TypeExpression with Container[AggregateDefinition] {
def contents: Seq[AggregateDefinition]
lazy val fields: Seq[Field] = contents.filter(_.isInstanceOf[Field]).asInstanceOf[Seq[Field]]
lazy val methods: Seq[Method] = contents.filter(_.isInstanceOf[Method]).asInstanceOf[Seq[Method]]
override def format: String = s"{ ${contents.map(_.format).mkString(", ")} }"
override def isAssignmentCompatible(other: TypeExpression): Boolean = {

other match {
case oate: AggregateTypeExpression =>
val validity: Seq[Boolean] = for
ofield <- oate.fields
myField <- fields.find(_.id.value == ofield.id.value)
ofield <- oate.contents
myField <- contents.find(_.id.value == ofield.id.value)
myTypEx = myField.typeEx
oTypeEx = ofield.typeEx
yield {
myTypEx.isAssignmentCompatible(oTypeEx)
}
(validity.size == oate.fields.size) && validity.forall(_ == true)
(validity.size == oate.contents.size) && validity.forall(_ == true)
case _ =>
super.isAssignmentCompatible(other)
}
Expand All @@ -289,7 +341,8 @@ trait Types {
* @param fields
* The fields of the aggregation
*/
case class Aggregation(loc: At, fields: Seq[Field] = Seq.empty[Field]) extends AggregateTypeExpression
case class Aggregation(loc: At, contents: Seq[AggregateDefinition] = Seq.empty[AggregateDefinition]) extends
AggregateTypeExpression

object Aggregation {
def empty(loc: At = At.empty): Aggregation = { Aggregation(loc) }
Expand Down Expand Up @@ -391,7 +444,7 @@ trait Types {
case class AggregateUseCaseTypeExpression(
loc: At,
usecase: AggregateUseCase,
fields: Seq[Field] = Seq.empty[Field]
contents: Seq[AggregateDefinition] = Seq.empty[AggregateDefinition]
) extends AggregateTypeExpression {
override def format: String = {
usecase.format.toLowerCase() + " " + super.format
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -376,20 +376,36 @@ private[parsing] trait TypeParser extends CommonParser {
def field[u: P]: P[Field] = {
P(
location ~ identifier ~ is ~ fieldTypeExpression ~ briefly ~ description
)
.map(tpl => (Field.apply _).tupled(tpl))
).map(tpl => (Field.apply _).tupled(tpl))
}

def arguments[u:P]: P[Seq[MethodArgument]] = {
P(
(
location ~ identifier.map(_.value) ~ Punctuation.colon ~ fieldTypeExpression
).map(tpl => (MethodArgument.apply _).tupled(tpl))
).rep(min=0, Punctuation.comma)
}

def fields[u: P]: P[Seq[Field]] = {
def method[u: P]: P[Method] = {
P(
location ~ identifier ~ Punctuation.roundOpen ~ arguments ~ Punctuation.roundClose ~
is ~ fieldTypeExpression ~ briefly ~ description
).map(tpl => (Method.apply _).tupled(tpl))
}


def aggregateDefinitions[u:P]: P[Seq[AggregateDefinition]] = {
P(
Punctuation.undefinedMark.!.map(_ => Seq.empty[Field]) |
field.rep(min = 0, Punctuation.comma)
undefined(Seq.empty[AggregateDefinition]) |
(field | method).rep(min = 1, Punctuation.comma)
)
}

def aggregation[u: P]: P[Aggregation] = {
P(location ~ Keywords.fields.? ~ open ~ fields ~ close).map { case (loc, fields) =>
Aggregation(loc, fields)
P(location ~ open ~ aggregateDefinitions.? ~ close).map {
case (loc, Some(contents)) => Aggregation(loc, contents)
case (loc, None) => Aggregation(loc, Seq.empty[AggregateDefinition])
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,33 @@ class TypeParserTest extends ParsingTest {
)
checkDefinition[Type, Type](rip, expected, identity)
}
"allow methods in aggregates" in {
val rip = RiddlParserInput("""record agg = {
| key: Number,
| calc(key: Number): Number,
|}
|""".stripMargin)
val expected = Type(
(1, 1, rip),
Identifier((1, 6, rip), "agg"),
Aggregation(
(1, 12, rip),
Seq(
Field(
(2, 3, rip),
Identifier((2, 3, rip), "key"),
Number((2, 8, rip))
),
Method(
(3, 3, rip),
Identifier((3, 3, rip), "calc"),
Seq(MethodArgument((3, 8, rip), "key", Number((3, 13, rip)))),
Number((3, 22, rip))
)
)
)
)
}
"allow command, event, query, and result message aggregations" in {
for mk <- Seq("command", "event", "query", "result") do {
val prefix = s"type mkt = $mk {"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -407,9 +407,9 @@ case class ResolutionPass(input: PassInput) extends Pass(input) with UsageResolu
case Enumeration(_, enumerators) =>
// if we're at an enumeration type then the numerators are candidates
enumerators
case AggregateUseCaseTypeExpression(_, _, fields) =>
case AggregateUseCaseTypeExpression(_, _, contents) =>
// Any kind of Aggregate's fields are candidates for resolution
fields
contents
case AliasedTypeExpression(_, pid) =>
// if we're at a field that references another type then the candidates
// are that type's fields. To solve this we need to push
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -227,7 +227,7 @@ class ResolutionPassTest extends ResolvingTest {
"""domain A {
| type T is { tp: A.TPrime } // Refers to T.TPrime
| type TPrime is { t: A.T } // Refers to A.T cyclically
| command DoIt is {}
| command DoIt is { }
| context C {
| entity E {
| record fields is {
Expand Down
Loading