Type inheritance
OSC2 defines five extensible types subject to inheritance:
- Enumerated types
- Structs
- Actors
- Scenarios
- Modifiers
There are four different inheritance-related mechanisms that you can apply:
- Extension
- Unconditional inheritance
- Conditional inheritance
- The in modifier
The applicability of these mechanisms to the various extensible classes are summarized in the following table.
| Extensible type | Extension | Unconditional inheritance | Conditional inheritance | in modifier |
|---|---|---|---|---|
| Struct | yes | yes | yes | no |
| Actor | yes | yes | yes | no |
| Scenario | yes | no | yes | yes |
| Modifier | yes | yes | yes | yes |
Extension
Extending a type allows you to add features to a type while retaining its name. All instances of a type are endowed with the union of features declared in all that type's extensions. Features in an extension cannot shadow previously declared features, though some features, like method declarations, allow overrides.
Unlike inheritance (where you define a new type), extension modifies the type being extended. All instances of the type get the newly added attributes. For instance, adding a mass attribute to the vehicle actor adds this attribute to every vehicle in every scenario.
Extending is particularly helpful when you have a library of inter-related actors / scenarios, and you want to add some attributes, constraints, scenarios or other features to accommodate project-specific needs.
Unconditional type inheritance
OSC2 implements single inheritance between extensible classes. This works like inheritance in most OO programming languages: a new subtype is declared, endowed with all the features of the parent type (supertype). Both types are accessible. Modifications to the supertype are automatically applied to the subtype. Changes to the subtype do not affect the supertype.
Any added members must, in combination with the members defined in the base type, fulfill all relevant restrictions for the base type.
struct|actor <type-name> inherits <supertype-name>:
<members>
In this example, the subtype junction inherits from the supertype road_element.
struct junction inherits road_element:
...
Conditional type inheritance
Conditional inheritance enables the creation of a subtype that depends upon the value of a Boolean or enumerated field. The condition always sets a single field to a specific value. The field value (the determinant) is a constant literal that is fixed during execution.
Conditional inheritance is specified by declaring the field and value that defines the dynamic subtype:
struct|actor|scenario|modifier <type-name> inherits <supertype-name>(<field> == <value>):
<members>
For example, the actor std_vehicle has the following attributes:
enum std_vehicle_category: [truck, car, motorcycle]
actor std_vehicle:
category: std_vehicle_category
emergency_vehicle: bool
See complete example.
Assuming the above declaration, you can define:
actor std_car inherits std_vehicle(category == car):
speed: speed
actor police_car inherits std_car(emergency_vehicle == true):
siren: bool
See complete example.
Conditional inheritance has two advantages over unconditional inheritance:
- An object can inherit features from multiple orthogonal conditional types.
- A generateable field can be declared as a type, but be assigned a dynamic subtype by the generator.
These capabilities are discussed below.
Relations between conditional and unconditional inheritance
The rule governing the relationship between these two kinds of inheritance is as follows:
Rule 1: A conditional type cannot be inherited unconditionally.
Inheritance relationships form a tree whose trunk is the predefined OSC2 types. Main limbs are inherited unconditionally. The final branches of the unconditional inheritance tree can be the roots of conditionally inherited sub-trees.
Relations between conditional subtypes
Any pair of conditional subtypes (types conditionally inherited from the same supertype) have one of the following relationships:
- One of the types can be a supertype of the other; or
- Both types have a common supertype. (They are orthogonal.)
Type membership
Consider a field declared as type std_vehicle. It may be assigned an object whose emergency_vehicle field is set to true.
Because the field is declared as type std_vehicle, its value must be an instance of std_vehicle, and only features of std_vehicle are accessible. However, OSC2 allows type-membership checking, using the is() operator.
Using type checking, you can access features under an active subtype, even though it is not the declared type. Such active but undeclared subtypes (police_car in this example) are called latent subtypes. This is summarized by the following two rules:
- Rule 2: The declared type determines an object's type membership.
- Rule 3: Dynamic type check allows access to latent subtype features.
Preventing type re-convergence
Type re-convergence occurs when two orthogonal subtypes are combined. This means the resulting type has multiple supertypes. This is not allowed in a single-inheritance scheme.
Consider the following example. In addition to the above std_vehicle declaration, add the following:
actor std_truck inherits std_vehicle(category == truck):
gears: int
actor fire_truck inherits std_truck(emergency_vehicle == true):
siren: bool
See complete example.
It might be possible to declare a type called, for example, first_responder, that includes both police_car and fire_truck. That hypothetical type would inherit from both, with a determinant that is a Boolean expression:
# this is not allowed in OSC2 semantics
emergency_vehicle == true and (category == car or category == truck)
OSC2 semantics disallows such complex determinants, in order to prevent type re-convergence. This is expressed in the following rule:
Rule 4: Each field can occur at most once in a conditional type determinant.
Note that it is possible to type check an object dynamically in order to determine if it's an emergency vehicle.
Type checking with the is() operator
<path-to-object>.is(<type-name>)
extend top.main:
car1: vehicle
var its_a_truck: bool = car1.is(box_truck)
See complete example.
is() returns true if the object is of the specified type, false otherwise. is() can be used in any context that accepts a Boolean expression.
Type casting with the as() operator
<path-to-object>.as(<type-name>)
extend box_truck:
num_of_wheels: int with:
keep(soft it == 6)
extend top.main:
keep(sut.car.vehicle_category == box_truck)
do serial:
sut.car.drive(duration: 10sec)
log("Truck SUT has $(sut.car.as(box_truck).num_of_wheels) wheels")
See complete example.
as() performs a cast, declaring the type of the object to be the specified type-name. The casting expression type has to have the same base type as the target expression type.
If during runtime the actual instance does not have the required type, an exception is issued. After the cast, the original expression is recognized as having the type requested in the cast operation.
See Type casting for a complete description of type casting.
Scenario and modifier inheritance
The rules for scenario and modifier inheritance are the same; "scenario" is used below to refer to both.
Scenario inheritance rules are compounded because scenarios are features of their actors, but they are also types by themselves. The inheritance process is described by the following steps:
- An actor subtype (conditional or unconditional) inherits its supertype scenarios.
- It then can extend the inherited scenarios, modifying their behavior.
- It can also declare new scenarios that can inherit conditionally from any of its scenarios (that is, other scenarios of the type or any of its supertypes).
When extending a scenario, it is possible to extend its behavior by adding a do <scenario invocation>. Multiple do invocations are performed in sequence, starting with the do invocation in the base type.
The following examples illustrate some options:
# fast_drive scenario defined under V10_vehicle
actor V10_vehicle:
first_responder: bool
scenario V10_vehicle.fast_drive:
high_speed: speed with:
keep(it in [105..130]kph)
actor police_V10 inherits V10_vehicle(first_responder == true)
# police_V10 inherits fast_drive() because it is a conditional subtype of V10_vehicle.
extend police_V10.fast_drive:
emergency_lights: bool
# the scram() scenario conditionally inherits from police_V10.fast_drive().
scenario police_V10.scram inherits fast_drive(emergency_lights == true):
siren: bool
keep(siren == true)
See complete example.
The in modifier affects the type system
While in is described as a modifier in OSC2, it has properties that affect the type system.
in <path-expression> with:
<members>
The in path designates a scenario instance to which the members are applied. Some possible members, like on, cover, synchronize and until are changing the underlying type of the scenario instance, essentially creating a prototype. (In the JavaScript sense, a prototype is a subtype that's specific to an instance).
The following rule applies to the use of the in modifier:
Rule 5: The application of in is static. Although the modifier may appear in some temporal context (such as the invocation of a scenario), the effect of it is static and global. The affected scenario instance will be endowed with the in properties throughout its existence.
This is implemented in the type system by creating a latent subtype of the affected scenario type. That latent subtype will have the in members. The particular instance affected by the in will be statically typed as the latent subtype.