Skip to content

Commit

Permalink
Implemented choice switch (finos#850)
Browse files Browse the repository at this point in the history
* Implemented choice switch

* Added docs

* Fixed compilation error

* Corrected docs

* Corrected docs

* Fixed

* Fixed circular ref issue

* Fixed bugs

* Fixed docs

* Typos
  • Loading branch information
SimonCockx authored Oct 2, 2024
1 parent 9ff8e53 commit caea5e2
Show file tree
Hide file tree
Showing 43 changed files with 1,271 additions and 356 deletions.
90 changes: 80 additions & 10 deletions docs/rune-modelling-component.md
Original file line number Diff line number Diff line change
Expand Up @@ -912,31 +912,101 @@ Rune supports basic arithmetic operators

The `default` operator takes two values of matching type. If the value to the left of the default is empty then the value of to the right of the default will be returned. Note that the type and cardinality of both sides of the operator must match for the syntax to be valid.

### Switch Operator
#### Switch Operator

The `switch` operator takes as its left hand input an argument on which to perform case analysis. The right side of the operator takes a set of case statements which define a return value for the expression when matching that case to the input.

```Haskell
"valueB" switch
"valueA" then "resultA"
"valueB" then "resultB"
"valueA" then "resultA",
"valueB" then "resultB",
default "resultC"
```

The `switch` operator can also operate over enumerations and in the case where all enumeration values are not provided as case statements then a syntax validation error will occur until either all enumeration values or a default value is provided.

```Haskell
"aCondition" switch
"aCondition" then SomeEnum -> A,
"bCondition" then SomeEnum -> B,
"cCondition" then SomeEnum -> C,
default SomeEnum -> D
enumInput switch
A then "result A",
B then "result B",
C then "result C",
default "other"
```

{{< notice info "Note" >}}
The `default` case is optional, in case when there is no match then the `empty` value is returned.
The `default` case is optional. In case when there is no match then the `empty` value is returned.
{{< /notice >}}

##### `switch` for `choice` types

Consider the following model of a powered vehicle.

``` Haskell
choice PoweredVehicle:
PetrolCar
ElectricCar
Motorcycle

type PetrolCar:
fuelCapacity number (1..1)

type ElectricCar:
batteryCapacity number (1..1)

type Motorcycle:
fuelCapacity number (1..1)
```

The `switch` operator supports case analysis on such a choice type as well. For example, consider the following simplified computation to estimate the mileage of a powered vehicle:

``` Haskell
func ComputeMileage:
inputs:
vehicle PoweredVehicle (1..1)
output:
mileage number (1..1)

set mileage:
vehicle switch
PetrolCar then 15 * fuelCapacity, // assume 15 kilometres per litre of fuel
ElectricCar then 5 * batteryCapacity, // assume 5 kilometres per kWh of battery
default 80 // for any other powered vehicle, assume a mileage of 80 kilometres
```

Note that within each case, you can access attributes specific to that case directly. The keyword `item` can be used to refer to the actual specific vehicle inside each case.

Performing case analysis on nested choice types is supported as well. As an illustration, consider the following extension of previous example.

``` Haskell
choice Vehicle:
PoweredVehicle // as defined above
Bicycle

type Bicycle:
weight number (1..1)
```

We could then extend our mileage computation to support all possible `Vehicle`s.

``` Haskell
func ComputeMileage:
inputs:
vehicle Vehicle (1..1)
output:
mileage number (1..1)

set mileage:
vehicle switch
PetrolCar then 15 * fuelCapacity, // assume 15 kilometres per litre of fuel
ElectricCar then 5 * batteryCapacity, // assume 5 kilometres per kWh of battery
PoweredVehicle then 80, // for any other powered vehicle, assume a mileage of 80 kilometres
Bicycle then 30 // assume a mileage of 30 kilometres for a bicycle
```

Even though the `Vehicle` choice type does not include `PetrolCar` directly, it is included indirectly through the `PoweredVehicle` choice type, and thus can be used as a case.

Similarly to enumerations, the syntax enforces you to cover all cases - or to add a `default` case at the end. For example, leaving out the `Bicycle` case in the example above will result in the `switch` operation being highlighted in red.

#### Operator Precedence

Expressions are evaluated in Rune in the following order, from first to last - see [Operator Precedence](https://en.wikipedia.org/wiki/Order_of_operations)).
Expand All @@ -954,7 +1024,7 @@ Expressions are evaluated in Rune in the following order, from first to last - s
1. and - e.g. `5>6 and true`
1. or - e.g. `5>6 or true`

### List
#### List

A list is an ordered collection of items of the same data type (basic, complex or enumeration). A path expression that refers to an attribute with multiple [cardinality](#cardinality) will result in a list of values.

Expand Down
46 changes: 32 additions & 14 deletions rosetta-lang/model/RosettaExpression.xcore
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import com.regnosys.rosetta.rosetta.RosettaCallableWithArgs
import com.regnosys.rosetta.rosetta.RosettaMapTestExpression
import com.regnosys.rosetta.rosetta.RosettaTyped
import com.regnosys.rosetta.rosetta.simple.Attribute
import com.regnosys.rosetta.rosetta.simple.ChoiceOption
import org.eclipse.emf.common.util.BasicEList
import com.regnosys.rosetta.rosetta.RosettaEnumValue

Expand Down Expand Up @@ -360,6 +361,37 @@ class ToDateTimeOperation extends ParseOperation {
class ToZonedDateTimeOperation extends ParseOperation {
}

class SwitchOperation extends RosettaUnaryOperation {
contains SwitchCase[] cases opposite switchOperation
contains RosettaExpression ^default
}
class SwitchCase {
container SwitchOperation switchOperation opposite cases
contains SwitchCaseGuard guard opposite ^case
contains RosettaExpression expression
}
class SwitchCaseGuard {
container SwitchCase ^case opposite guard

contains RosettaLiteral literalGuard

refers RosettaSymbol symbolGuard
refers derived RosettaEnumValue enumGuard get {
val s = symbolGuard
if (s instanceof RosettaEnumValue) {
return s
}
return null
}
refers derived ChoiceOption choiceOptionGuard get {
val s = symbolGuard
if (s instanceof ChoiceOption) {
return s
}
return null
}
}

/**
* Functional operations
*/
Expand Down Expand Up @@ -406,17 +438,3 @@ class MinOperation extends ComparingFunctionalOperation, ListOperation {

class MaxOperation extends ComparingFunctionalOperation, ListOperation {
}


class SwitchOperation extends RosettaUnaryOperation {
contains SwitchCase[] cases opposite switchOperation
contains RosettaExpression ^default
}

class SwitchCase {
container SwitchOperation switchOperation opposite cases
contains RosettaLiteral literalGuard
refers RosettaEnumValue enumGuard
contains RosettaExpression expression
}

5 changes: 5 additions & 0 deletions rosetta-lang/model/RosettaSimple.xcore
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import com.regnosys.rosetta.rosetta.RosettaFactory
import com.regnosys.rosetta.rosetta.expression.ExpressionFactory

import org.eclipse.xtext.nodemodel.util.NodeModelUtils
import org.eclipse.emf.common.util.EList
import com.regnosys.rosetta.rosetta.RosettaPackage.Literals

abstract class RootElement extends RosettaRootElement, RosettaNamed, RosettaDefinable, Annotated {
Expand Down Expand Up @@ -78,6 +79,10 @@ class Data extends RosettaType, RootElement, References {
class Choice extends Data {
contains Condition[] _hardcodedConditions

contains derived ChoiceOption[] options get {
attributes as EList
}

op Condition[] getConditions() {
if (_hardcodedConditions.empty) {
val cond = SimpleFactory.eINSTANCE.createCondition
Expand Down
9 changes: 6 additions & 3 deletions rosetta-lang/src/main/java/com/regnosys/rosetta/Rosetta.xtext
Original file line number Diff line number Diff line change
Expand Up @@ -638,9 +638,12 @@ enum ExistsModifier:
;

SwitchCase:
(literalGuard=RosettaLiteral | enumGuard=[RosettaEnumValue|ValidID]) 'then' expression=RosettaCalcExpression
;

guard=SwitchCaseGuard 'then' expression=RosettaCalcExpression
;

SwitchCaseGuard:
literalGuard=RosettaLiteral | symbolGuard=[RosettaSymbol|ValidID]
;

UnaryOperation returns RosettaExpression:
RosettaCalcPrimary
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ import com.regnosys.rosetta.rosetta.simple.SimpleFactory
import com.regnosys.rosetta.types.RObjectFactory
import java.util.LinkedHashSet
import com.regnosys.rosetta.types.TypeSystem
import com.regnosys.rosetta.types.RChoiceType

@Singleton // see `metaFieldsCache`
class RosettaEcoreUtil {
Expand All @@ -53,6 +54,8 @@ class RosettaEcoreUtil {
switch t {
RDataType:
t.allNonOverridenAttributes.map[EObject]
RChoiceType:
t.asRDataType.allFeatures(resourceSet)
REnumType:
t.allEnumValues
RRecordType: {
Expand Down Expand Up @@ -259,7 +262,7 @@ class RosettaEcoreUtil {
def List<RAttribute> allJavaAttributes(RDataType t) {
val atts = t.javaAttributes
if (t.superType !== null) {
val attsWithSuper = (t.superType.stripFromTypeAliases as RDataType).allJavaAttributes
val attsWithSuper = t.superType.allJavaAttributes
val result = newArrayList
attsWithSuper.forEach[
val overridenAtt = atts.findFirst[att| att.name == name]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import com.regnosys.rosetta.generator.java.types.JavaTypeUtil
import java.util.HashSet
import com.regnosys.rosetta.generator.java.statement.builder.JavaVariable
import com.regnosys.rosetta.types.RAttribute
import com.regnosys.rosetta.types.RChoiceType

class DeepPathUtilGenerator {
@Inject extension ImportManagerExtension
Expand Down Expand Up @@ -55,9 +56,13 @@ class DeepPathUtilGenerator {
val recursiveDeepFeaturesMap = choiceType.allNonOverridenAttributes.toMap([it], [
val attrType = it.RType
deepFeatures.toMap([it], [
if (attrType instanceof RDataType) {
if (attrType.findDeepFeatureMap.containsKey(it.name)) {
dependencies.add(attrType.toDeepPathUtilJavaClass)
var t = attrType
if (t instanceof RChoiceType) {
t = t.asRDataType
}
if (t instanceof RDataType) {
if (t.findDeepFeatureMap.containsKey(it.name)) {
dependencies.add(t.toDeepPathUtilJavaClass)
return true
}
}
Expand Down Expand Up @@ -109,7 +114,10 @@ class DeepPathUtilGenerator {
val deepFeatureExpr = if (deepFeature.match(a)) {
attrVar
} else {
val attrType = a.RType
var attrType = a.RType
if (attrType instanceof RChoiceType) {
attrType = attrType.asRDataType
}
val needsToGoDownDeeper = recursiveDeepFeaturesMap.get(a).get(deepFeature)
val actualFeature = if (needsToGoDownDeeper || !(attrType instanceof RDataType)) {
deepFeature
Expand Down
Loading

0 comments on commit caea5e2

Please sign in to comment.