diff --git a/docs/dev/stdlib-worksheets/08-09-movement-modifiers.md b/docs/dev/stdlib-worksheets/08-09-movement-modifiers.md new file mode 100644 index 0000000..2a33282 --- /dev/null +++ b/docs/dev/stdlib-worksheets/08-09-movement-modifiers.md @@ -0,0 +1,123 @@ +# §8.9 — movement modifiers + +Source: ASAM OpenSCENARIO DSL 2.2.0, `domain-model` §8.9 (Tables 147–153, plus +Code 78–120). Landed in slice 43g. Target chunk: `kDomainMovementModifiers` in +`frontends/dsl/src/stdlib.cpp`. + +§8.9 is presented differently from the rest of §8: each modifier gets a *usage +signature* and a prose parameter list rather than a parameter table, and the +shape hierarchy is printed as DSL. Only the seven enums are tables. + +Unlike §8.8, stripping the `Examples` blocks does not help here — the examples +are inline in each section. The whole chapter has to be read. + +## The four common parameters (§8.9.1.1) + +> The following parameters are common to all domain model movement modifiers. + +| Parameter | Type | § | +| --- | --- | --- | +| `at` | `at` | 8.9.1.1.1 | +| `movement_mode` | `movement_mode` | 8.9.1.1.2 | +| `track` | `track` | 8.9.1.1.3 | +| `shape` | `any_shape` | 8.9.1.1.4 | + +Every modifier below carries all four. ⚠ Five usage signatures +(`keep_position`, `keep_speed`, `keep_lane`, `physical_movement`, +`avoid_collisions`) omit the `` placeholder that +the other twelve include. §8.9.1.1's "common to all" is the general normative +statement and the usage blocks are illustrative of a typical call, so all +seventeen get them — and over-accepting is the safer error here, since the +alternative rejects a scenario the standard permits. + +## Shape hierarchy (§8.9.1.2, printed as DSL) + +| Struct | Base | Members | +| --- | --- | --- | +| `any_shape` | — | `def duration() -> time` | +| `any_acceleration_shape` | `any_shape` | `def compute(time: time) -> acceleration` | +| `any_speed_shape` | `any_shape` | `def compute(time: time) -> speed` | +| `any_position_shape` | `any_shape` | `def compute(time: time) -> length` | +| `any_lateral_shape` | `any_shape` | `def compute(time: time) -> length` | +| `common_acceleration_shape` | `any_acceleration_shape` | `rate_profile: dynamic_profile`, `rate_peak: jerk`, `target: acceleration` | +| `common_speed_shape` | `any_speed_shape` | `rate_profile`, `rate_peak: acceleration`, `target: speed` | +| `common_position_shape` | `any_position_shape` | `rate_profile`, `rate_peak: speed`, `target: length` | +| `common_lateral_shape` | `any_lateral_shape` | `rate_profile`, `rate_peak: speed`, `target: length` | + +## Enums (§8.9.19–§8.9.25) + +| § | Enum | Members | ⚠ | +| --- | --- | --- | --- | +| .19 | `at` | `start, end, all` | a field named `at` of type `at` on every modifier | +| .20 | `movement_mode` | `monotonous, other` | `other` is in ~six other enums | +| .21 | `track` | `actual, projected` | | +| .22 | `lat_measure_by` | `left_to_left … right_to_right, closest` (10) | | +| .23 | `yaw_measure_by` | `length_to_length, length_to_width, width_to_length, width_to_width, relative_to_north, relative_to_road` | | +| .24 | `orientation_measured_by` | `absolute, relative_to_reference, relative_to_road` | `relative_to_road` shared with `yaw_measure_by` | +| .25 | `movement_options` | `prefer_physical, prefer_non_physical, must_be_physical` | | + +## Modifiers (§8.9.2–§8.9.18) + +Association per §7.3.12.3 — see note 1. `+4` marks the four common parameters. + +| § | Modifier | Declared on | Own parameters | +| --- | --- | --- | --- | +| .2 | `position` | `movable_object` | `distance: length`, `time: time`, `distance_range: range of length`, `time_range: range of time`, `ahead_of: physical_object`, `behind: physical_object`, `ahead_of_point: position_3d`, `behind_point: position_3d`, `at_point: position_3d`, `project_on_route: bool` | +| .3 | `keep_position` | `movable_object` | — (+4) | +| .4 | `speed` | `movable_object` | `speed: speed`, `speed_range: range of speed`, `faster_than`/`slower_than`/`same_as: physical_object`, `factor: float`, `direction: lon_lat` | +| .5 | `change_speed` | **unassociated** | `speed: speed`, `speed_range: range of speed` | +| .6 | `keep_speed` | **unassociated** | — (+4) | +| .7 | `acceleration` | `movable_object` | `acceleration: acceleration`, `acceleration_range: range of acceleration` | +| .8 | `lateral` | `movable_object` | `distance: length`, `distance_range: range of length`, `side_of: vehicle`, `side: side_left_right`, `measure_by: lat_measure_by` | +| .9 | `yaw` | `movable_object` | `angle: angle`, `angle_range: range of angle`, `relative_to: physical_object`, `measure_by: yaw_measure_by` | +| .10 | `orientation` | `movable_object` | `yaw: angle`, `pitch: angle`, `roll: angle`, `relative_to: physical_object`, `measure_by: orientation_measured_by` | +| .11 | `along` | `movable_object` | `route: route`, `start_offset: length`, `end_offset: length` | +| .12 | `along_trajectory` | `movable_object` | `trajectory: trajectory`, `start_offset: length`, `end_offset: length` | +| .13 | `distance` | `movable_object` | `distance: length` | +| .14 | `lane` | `vehicle` | `lane: uint`, `side_of: physical_object`, `side: side_left_right`, `same_as: physical_object`, `from: side_left_right` | +| .15 | `change_lane` | **unassociated** | `lane: int`, `side: side_left_right` | +| .16 | `keep_lane` | `vehicle` | — (+4) | +| .17 | `physical_movement` | `movable_object` | `option: movement_options` | +| .18 | `avoid_collisions` | `movable_object` | `avoid: bool` | + +## ⚠ Notes carried into the library + +1. **These modifiers are actor-associated, and the language reference says so + outright.** §7.3.12.3's example of an actor-associated modifier is + `modifier vehicle.keep_lane()`, annotated *"keep_lane() is defined in the + domain model (see §8.9.16)"* — §7.3 is telling us how §8.9 is declared. + Each modifier goes on the most general actor that can execute the movement + actions it tunes: `movable_object`, which owns `move()` and parents both + `vehicle` and `person`, or `vehicle` for the lane-related ones (`lane`, + `keep_lane`), which §7.3.12.3 places there explicitly. +2. **Three modifiers must be unassociated, because the standard collides with + itself.** §8.8 declares actions `movable_object.change_speed`, + `movable_object.keep_speed` and `vehicle.change_lane`; §8.9 declares + modifiers of the same names for the same actors. A qualified behavior name + (§7.2.2.2.5) identifies exactly one declaration, so the language cannot hold + both. Declaring these three in §7.3.12.3's *unassociated* form is the only + spelling that exists in the language and does not collide. The defect is in + the standard, not in the translation, and the choice is recorded in the + coverage matrix as well as here. +3. **Declaring them associated is also what avoids two further collisions.** + An unassociated `modifier lane` collides head-on with §8.12.10's + `struct lane`, and an unassociated `modifier speed` shadows + `stdtypes::speed` badly enough that `speed_range: range of speed` stops + resolving to a physical type. Under `modifier vehicle.lane` and + `modifier movable_object.speed` the declared names are qualified and neither + problem arises. This is corroborating evidence for note 1: association is + not decoration, it is what makes the chapter declarable. +4. **§8.9.14's `lane` counts lanes as `uint`, §8.9.15's `change_lane` as + `int`.** Both carried as printed. +5. **Applying any of these is issue #100**, in both of §7.3.12.4.1's positions: + as a scenario member, an actor-associated modifier is not found by simple + name; inside a `with:` block, modifier applications are not validated at all + (a nonsense name is accepted silently). The declarations here are + well-formed and are pinned; nothing pins application, because neither + current behaviour is one to keep. p8-s3 (#46) needs #100 fixed first, and + §8.9 is the surface that fix should be tested against. +6. **§8.9.1.4's scalar/range pairs are two separate fields.** `speed` and + `speed_range`, `distance` and `distance_range`, `angle` and `angle_range`: + "at most one of them is used within an invocation", which is a constraint on + applications rather than on the declaration. Same rule as §8.8's mutually + exclusive parameters. diff --git a/docs/dev/stdlib-worksheets/README.md b/docs/dev/stdlib-worksheets/README.md index d18ad0a..3717ced 100644 --- a/docs/dev/stdlib-worksheets/README.md +++ b/docs/dev/stdlib-worksheets/README.md @@ -28,6 +28,7 @@ translated. | [`08-12-02-map.md`](08-12-02-map.md) | §8.12.2 actor `map` | 43e | | [`08-15-traffic-lights.md`](08-15-traffic-lights.md) | §8.15 traffic lights | 43h | | [`08-08-movement-actions.md`](08-08-movement-actions.md) | §8.8.2–§8.8.4 movement actions | 43f | +| [`08-09-movement-modifiers.md`](08-09-movement-modifiers.md) | §8.9 movement modifiers | 43g | ## Reading a chapter without reading all of it @@ -42,6 +43,9 @@ sed 's/\\//g' domain-model.md | awk 'NR>=2930 && NR<=5120' \ | awk '/^##### .* Examples/{skip=1} /^#### /{skip=0} !skip' ``` +This does not always help: §8.9 puts its examples inline in each section rather +than in `Examples` subsections, so that chapter has to be read whole. + The examples are still worth a look afterwards — §8.15.10's showed which actor the traffic-light actions hang off, and §8.12.2's showed that `create_route` is called with fewer arguments than it declares — but they are a second pass, not diff --git a/docs/roadmap/coverage/osc-dsl-coverage.md b/docs/roadmap/coverage/osc-dsl-coverage.md index 5f9a864..32a961b 100644 --- a/docs/roadmap/coverage/osc-dsl-coverage.md +++ b/docs/roadmap/coverage/osc-dsl-coverage.md @@ -114,7 +114,7 @@ included, so a conforming implementation and Scena agree. | `std` — action hierarchy and the environment | §8.8.1, §8.10, §8.11 | In | p7-s5 | **Landed** (`dsl_stdlib_test.cpp`): `osc_action` with `action_for_environment` and `action_for_movable_object`; the `environment` actor with `local_to_unix_time`; the `weather`/`air`/`precipitation`/`wind`/`fog`/`clouds`/`celestial_light_source` structs; the seven §8.11 environment actions. Their *execution* stays Post (see the actor table) — the DSL environment actions are checked, not run, in v0.0.1 | | `std` — traffic lights | §8.15 | In | p7-s5 | **Landed** (`dsl_stdlib_test.cpp`): `traffic_light_bulb`, `traffic_light` and `traffic_light_group` with their state methods, `traffic_light_stop_line` (a `route_element`), `traffic_light_phase`/`traffic_light_cycle`, the `traffic_light_controller` actor and its seven §8.15.9 actions, and 5 enums. Their *execution* stays Post (see the actor table). Worksheet: `docs/dev/stdlib-worksheets/08-15-traffic-lights.md` | | `std` — movement actions | §8.8.2–§8.8.4 | In | p7-s5 | **Landed** (`dsl_stdlib_test.cpp`): 15 actions for `movable_object`, 13 for `vehicle` and `walk` for `person`, plus `dynamic_profile`, `lane_change_side`, `gap_direction` and `headway_direction`. `action_for_vehicle` and `action_for_person` are declared here — §8.8.3/§8.8.4 name them as parents but only §8.8.1's prose defines them. Their *execution* stays Post except where the roadmap's action table says otherwise. Worksheet: `docs/dev/stdlib-worksheets/08-08-movement-actions.md` | -| `std` — movement modifiers | §8.3, §8.5, §8.9 | In | p7-s5 | Lands with the remaining slice of #43 | +| `std` — movement modifiers | §8.9 | In | p7-s5 | **Landed** (`dsl_stdlib_test.cpp`): the `any_shape`/`common_*_shape` hierarchy, the seven §8.9.19–§8.9.25 enums, and 17 modifiers each carrying §8.9.1.1's four common parameters (`at`, `movement_mode`, `track`, `shape`). Fourteen are actor-associated per §7.3.12.3; `change_speed`, `keep_speed` and `change_lane` are unassociated because §8.8 already declares actions of those names on those actors and a qualified behavior name identifies one declaration — the collision is the standard's. *Applying* a modifier is issue #100 in both of §7.3.12.4.1's positions. Worksheet: `docs/dev/stdlib-worksheets/08-09-movement-modifiers.md` | Coverage of the individual §8 declarations: all physical types and units (§8.14.1), compound structs (§8.14.2), string methods (§8.13), all actors diff --git a/frontends/dsl/README.md b/frontends/dsl/README.md index d1dc870..0aaceae 100644 --- a/frontends/dsl/README.md +++ b/frontends/dsl/README.md @@ -171,7 +171,17 @@ expected. It also keeps the dependency list unchanged. names a scenario or an action. Note that such a modifier cannot yet be *applied* — issue #100: the parser keeps the actor prefix in the declared name, so `check_modifier_application` looks up a name no scope holds. - Unassociated modifiers are unaffected. + Unassociated modifiers are unaffected, and a modifier application inside a + `with:` block is not checked at all — same issue. +- **Association is what makes §8.9 declarable.** §7.3.12.3's own example is + `modifier vehicle.keep_lane()`, annotated as being §8.9.16's, so the movement + modifiers are actor-associated. That is not decoration: an unassociated + `modifier lane` would collide with §8.12.10's `struct lane`, and an + unassociated `modifier speed` would shadow `stdtypes::speed` so thoroughly + that `range of speed` stops naming a physical type. Three modifiers must + still be unassociated — `change_speed`, `keep_speed`, `change_lane` — because + §8.8 declares *actions* of those names on those actors, and a qualified + behavior name identifies exactly one declaration. - **Every declaration name is a lookup space of its own.** The standard reuses words freely: a field `driving_rule` of type `driving_rule` (§8.12.2), a struct `air` and an action `environment.air` (§8.10.4 / §8.11.2), an enum diff --git a/frontends/dsl/src/stdlib.cpp b/frontends/dsl/src/stdlib.cpp index e254965..4226f81 100644 --- a/frontends/dsl/src/stdlib.cpp +++ b/frontends/dsl/src/stdlib.cpp @@ -1148,16 +1148,267 @@ action vehicle.disconnect_trailer inherits vehicle.action_for_vehicle action person.walk inherits person.action_for_person )OSC"; +// §8.9 the movement modifiers: the shape hierarchy §8.9.1.2 prints as DSL, +// the seven enums of §8.9.19-§8.9.25, and the seventeen modifiers that tune a +// movement action. Each modifier is printed as a usage signature plus a +// parameter list rather than as a table, so the arities below are the +// standard's own. +// Translation worksheet: docs/dev/stdlib-worksheets/08-09-movement-modifiers.md +constexpr std::string_view kDomainMovementModifiers = R"OSC( +# --- §8.9.1.2 the shape hierarchy -------------------------------------------- +# One of the few places §8 prints DSL rather than tables, so this is a +# transcription. A shape describes how a state variable moves as a function of +# time within one modifier invocation; `duration()` gives its extent and +# `compute()` the value at a time measured from the start of the phase. +struct any_shape: + def duration() -> time is undefined + +struct any_acceleration_shape inherits any_shape: + def compute(time: time) -> acceleration is undefined + +struct any_speed_shape inherits any_shape: + def compute(time: time) -> speed is undefined + +struct any_position_shape inherits any_shape: + def compute(time: time) -> length is undefined + +struct any_lateral_shape inherits any_shape: + def compute(time: time) -> length is undefined + +# The built-in common shapes: a target for the state variable plus two +# parameters for its first derivative. +struct common_acceleration_shape inherits any_acceleration_shape: + rate_profile: dynamic_profile + rate_peak: jerk + target: acceleration + +struct common_speed_shape inherits any_speed_shape: + rate_profile: dynamic_profile + rate_peak: acceleration + target: speed + +struct common_position_shape inherits any_position_shape: + rate_profile: dynamic_profile + rate_peak: speed + target: length + +struct common_lateral_shape inherits any_lateral_shape: + rate_profile: dynamic_profile + rate_peak: speed + target: length + +# --- §8.9.19–§8.9.25 the movement enums -------------------------------------- +enum at: [start, end, all] + +enum movement_mode: [monotonous, other] + +enum track: [actual, projected] + +enum lat_measure_by: [ + left_to_left, left_to_center, left_to_right, center_to_left, + center_to_center, center_to_right, right_to_left, right_to_center, + right_to_right, closest +] + +enum yaw_measure_by: [ + length_to_length, length_to_width, width_to_length, width_to_width, + relative_to_north, relative_to_road +] + +enum orientation_measured_by: [absolute, relative_to_reference, relative_to_road] + +enum movement_options: [prefer_physical, prefer_non_physical, must_be_physical] + +# --- §8.9.2–§8.9.18 the movement modifiers ----------------------------------- +# These are ACTOR-ASSOCIATED. §7.3.12.3's own example is `modifier +# vehicle.keep_lane()`, annotated "keep_lane() is defined in the domain model +# (see §8.9.16)" — the language reference states outright how this chapter's +# modifiers are declared. Each one goes on the most general actor that can run +# the movement actions it tunes: `movable_object`, which owns `move()` and +# parents both `vehicle` and `person`, or `vehicle` for the lane trio. +# +# THREE EXCEPTIONS take the unassociated form (§7.3.12.3's first association +# type): `change_speed`, `keep_speed` and `change_lane`. §8.8 already declares +# ACTIONS of exactly those names on exactly those actors, and a qualified +# behavior name identifies one declaration — the language cannot hold both +# `movable_object.change_speed` the action and `movable_object.change_speed` +# the modifier. The collision is in the standard, not in the translation. +# +# Every one of them carries §8.9.1.1's four common parameters — "The following +# parameters are common to all domain model movement modifiers": `at`, +# `movement_mode`, `track` and `shape`. Five of the usage blocks below omit the +# `` placeholder; §8.9.1.1's general statement is +# the normative one and accepting the parameter is the safer of the two +# readings, so they are declared throughout. + +modifier movable_object.position: + distance: length + time: time + distance_range: range of length + time_range: range of time + ahead_of: physical_object + behind: physical_object + ahead_of_point: position_3d + behind_point: position_3d + at_point: position_3d + project_on_route: bool + at: at + movement_mode: movement_mode + track: track + shape: any_shape + +modifier movable_object.keep_position: + at: at + movement_mode: movement_mode + track: track + shape: any_shape + +# §8.9.1.4: `speed` fixes one value for the whole action; `speed_range` lets it +# vary within the interval. At most one of the pair may be given. +modifier movable_object.speed: + speed: speed + speed_range: range of speed + faster_than: physical_object + slower_than: physical_object + same_as: physical_object + factor: float + direction: lon_lat + at: at + movement_mode: movement_mode + track: track + shape: any_shape + +modifier change_speed: + speed: speed + speed_range: range of speed + at: at + movement_mode: movement_mode + track: track + shape: any_shape + +modifier keep_speed: + at: at + movement_mode: movement_mode + track: track + shape: any_shape + +modifier movable_object.acceleration: + acceleration: acceleration + acceleration_range: range of acceleration + at: at + movement_mode: movement_mode + track: track + shape: any_shape + +modifier movable_object.lateral: + distance: length + distance_range: range of length + side_of: vehicle + side: side_left_right + measure_by: lat_measure_by + at: at + movement_mode: movement_mode + track: track + shape: any_shape + +modifier movable_object.yaw: + angle: angle + angle_range: range of angle + relative_to: physical_object + measure_by: yaw_measure_by + at: at + movement_mode: movement_mode + track: track + shape: any_shape + +modifier movable_object.orientation: + yaw: angle + pitch: angle + roll: angle + relative_to: physical_object + measure_by: orientation_measured_by + at: at + movement_mode: movement_mode + track: track + shape: any_shape + +modifier movable_object.along: + route: route + start_offset: length + end_offset: length + at: at + movement_mode: movement_mode + track: track + shape: any_shape + +modifier movable_object.along_trajectory: + trajectory: trajectory + start_offset: length + end_offset: length + at: at + movement_mode: movement_mode + track: track + shape: any_shape + +modifier movable_object.distance: + distance: length + at: at + movement_mode: movement_mode + track: track + shape: any_shape + +# §8.9.14's `lane` counts lanes as a `uint`; §8.9.15's `change_lane` counts them +# as an `int`. Both carried as printed. +modifier vehicle.lane: + lane: uint + side_of: physical_object + side: side_left_right + same_as: physical_object + from: side_left_right + at: at + movement_mode: movement_mode + track: track + shape: any_shape + +modifier change_lane: + lane: int + side: side_left_right + at: at + movement_mode: movement_mode + track: track + shape: any_shape + +modifier vehicle.keep_lane: + at: at + movement_mode: movement_mode + track: track + shape: any_shape + +modifier movable_object.physical_movement: + option: movement_options + at: at + movement_mode: movement_mode + track: track + shape: any_shape + +modifier movable_object.avoid_collisions: + avoid: bool + at: at + movement_mode: movement_mode + track: track + shape: any_shape +)OSC"; + const std::string& types_module() { static const std::string source = std::string(kTypesScalars) + std::string(kTypesCompound); return source; } const std::string& domain_module() { - static const std::string source = std::string(kDomainEntities) + std::string(kDomainRoads) + - std::string(kDomainEnvironment) + - std::string(kDomainTrafficLights) + std::string(kDomainMap) + - std::string(kDomainMovementActions); + static const std::string source = + std::string(kDomainEntities) + std::string(kDomainRoads) + std::string(kDomainEnvironment) + + std::string(kDomainTrafficLights) + std::string(kDomainMap) + + std::string(kDomainMovementActions) + std::string(kDomainMovementModifiers); return source; } diff --git a/frontends/dsl/tests/dsl_stdlib_test.cpp b/frontends/dsl/tests/dsl_stdlib_test.cpp index ca0f41a..3641c79 100644 --- a/frontends/dsl/tests/dsl_stdlib_test.cpp +++ b/frontends/dsl/tests/dsl_stdlib_test.cpp @@ -1370,22 +1370,35 @@ TEST(DslStdlibTest, ADeprecatedParameterIsStillDeclared) { } TEST(DslStdlibTest, AnActionAndAModifierMayShareASimpleName) { - // `change_speed`, `keep_speed` and `change_lane` are actions here (§8.8) - // and modifiers in §8.9. An action's name is a qualified behavior name - // (§7.2.2.2.5), so `movable_object.change_speed` never collides with a - // plain `change_speed` — which is what lets §8.9 land beside this. + // `change_speed`, `keep_speed` and `change_lane` are actions in §8.8 and + // modifiers in §8.9. An action's name is a qualified behavior name + // (§7.2.2.2.5), so `movable_object.change_speed` the action and a plain + // `change_speed` the modifier are two declarations and coexist. + // + // What they cannot do is share the SAME qualified name, which is why these + // three modifiers are unassociated — see + // ThreeModifiersAreUnassociatedBecauseTheStandardCollides. Library library; check_full_library(library); - for (const char* action : {"std::movable_object.change_speed", "std::movable_object.keep_speed", - "std::vehicle.change_lane"}) { - const TypeInfo* type = library.program.find(action); - ASSERT_NE(type, nullptr) << action; - EXPECT_EQ(type->kind, TypeKind::Action) << action; - } - for (const char* unqualified : {"std::change_speed", "std::keep_speed", "std::change_lane"}) { - EXPECT_EQ(library.program.find(unqualified), nullptr) - << unqualified << " is §8.9's, and §8.9 has not landed yet"; + struct Case { + const char* action; + const char* modifier; + }; + for (const Case& expected : {Case{"std::movable_object.change_speed", "std::change_speed"}, + Case{"std::movable_object.keep_speed", "std::keep_speed"}, + Case{"std::vehicle.change_lane", "std::change_lane"}}) { + const TypeInfo* action = library.program.find(expected.action); + const TypeInfo* modifier = library.program.find(expected.modifier); + ASSERT_NE(action, nullptr) << expected.action; + ASSERT_NE(modifier, nullptr) << expected.modifier; + EXPECT_EQ(action->kind, TypeKind::Action) << expected.action; + EXPECT_EQ(modifier->kind, TypeKind::Modifier) << expected.modifier; } + // The same holds where the modifier IS associated: §8.9's speed modifier + // is `movable_object.speed`, and no action of that name exists to clash. + const TypeInfo* speed_modifier = library.program.find("std::movable_object.speed"); + ASSERT_NE(speed_modifier, nullptr); + EXPECT_EQ(speed_modifier->kind, TypeKind::Modifier); } TEST(DslStdlibTest, ADirectionLiteralNeedsItsEnumName) { @@ -1423,6 +1436,243 @@ TEST(DslStdlibTest, ADirectionLiteralNeedsItsEnumName) { : qualified.diagnostics().front().message); } +// --- §8.9 movement modifiers ------------------------------------------------ + +TEST(DslStdlibTest, TheShapeHierarchyIsRootedInAnyShape) { + // §8.9.1.2 is one of the few places §8 prints DSL rather than tables, so + // this is a transcription. A shape says how a state variable moves as a + // function of time within one modifier invocation. + Library library; + check_full_library(library); + const auto any_shape = library.program.types_by_name.find("std::any_shape"); + ASSERT_NE(any_shape, library.program.types_by_name.end()); + EXPECT_NE(library.program.find_method(any_shape->second, "duration"), nullptr); + struct Case { + const char* name; + const char* base; + const char* computes; + }; + for (const Case& expected : + {Case{"std::any_acceleration_shape", "std::any_shape", "stdtypes::acceleration"}, + Case{"std::any_speed_shape", "std::any_shape", "stdtypes::speed"}, + Case{"std::any_position_shape", "std::any_shape", "stdtypes::length"}, + Case{"std::any_lateral_shape", "std::any_shape", "stdtypes::length"}, + Case{"std::common_acceleration_shape", "std::any_acceleration_shape", nullptr}, + Case{"std::common_speed_shape", "std::any_speed_shape", nullptr}, + Case{"std::common_position_shape", "std::any_position_shape", nullptr}, + Case{"std::common_lateral_shape", "std::any_lateral_shape", nullptr}}) { + const auto id = library.program.types_by_name.find(expected.name); + const auto base = library.program.types_by_name.find(expected.base); + ASSERT_NE(id, library.program.types_by_name.end()) << expected.name; + ASSERT_NE(base, library.program.types_by_name.end()) << expected.base; + EXPECT_TRUE(library.program.is_derived_from(id->second, base->second)) << expected.name; + // Every shape inherits `duration()`; a concrete one adds `compute()`. + EXPECT_NE(library.program.find_method(id->second, "duration"), nullptr) << expected.name; + const scena::dsl::MethodInfo* compute = library.program.find_method(id->second, "compute"); + ASSERT_NE(compute, nullptr) << expected.name; + if (expected.computes != nullptr) { + ASSERT_NE(compute->return_type, scena::dsl::kInvalidType) << expected.name; + EXPECT_EQ(library.program.types[compute->return_type].name, expected.computes) + << expected.name; + } + } + // The common shapes add the target and its first-derivative controls. + const auto common_speed = library.program.types_by_name.find("std::common_speed_shape"); + ASSERT_NE(common_speed, library.program.types_by_name.end()); + for (const char* field : {"rate_profile", "rate_peak", "target"}) { + EXPECT_NE(library.program.find_field(common_speed->second, field), nullptr) << field; + } +} + +TEST(DslStdlibTest, TheMovementModifierEnumsCarryTheirValues) { + Library library; + check_full_library(library); + struct Case { + const char* name; + std::size_t values; + const char* sample; + }; + for (const Case& expected : + {Case{"std::at", 3, "all"}, Case{"std::movement_mode", 2, "monotonous"}, + Case{"std::track", 2, "projected"}, Case{"std::lat_measure_by", 10, "closest"}, + Case{"std::yaw_measure_by", 6, "relative_to_north"}, + Case{"std::orientation_measured_by", 3, "relative_to_reference"}, + Case{"std::movement_options", 3, "must_be_physical"}}) { + const TypeInfo* type = library.program.find(expected.name); + ASSERT_NE(type, nullptr) << expected.name; + EXPECT_EQ(type->kind, TypeKind::Enum) << expected.name; + EXPECT_EQ(type->enum_members.size(), expected.values) << expected.name; + bool found = false; + for (const auto& member : type->enum_members) { + found = found || member.name == expected.sample; + } + EXPECT_TRUE(found) << expected.name << "!" << expected.sample; + } +} + +TEST(DslStdlibTest, EveryMovementModifierCarriesTheCommonParameters) { + // §8.9.1.1: "The following parameters are common to all domain model + // movement modifiers." Five of the usage signatures omit the + // `` placeholder that the other twelve + // include; the general statement is the normative one, and accepting a + // parameter the standard permits beats rejecting it. + Library library; + check_full_library(library); + struct Case { + const char* parameter; + const char* type; + }; + for (const char* name : + {"std::movable_object.position", "std::movable_object.keep_position", + "std::movable_object.speed", "std::change_speed", "std::keep_speed", + "std::movable_object.acceleration", "std::movable_object.lateral", + "std::movable_object.yaw", "std::movable_object.orientation", "std::movable_object.along", + "std::movable_object.along_trajectory", "std::movable_object.distance", + "std::vehicle.lane", "std::change_lane", "std::vehicle.keep_lane", + "std::movable_object.physical_movement", "std::movable_object.avoid_collisions"}) { + const auto id = library.program.types_by_name.find(name); + ASSERT_NE(id, library.program.types_by_name.end()) << name; + EXPECT_EQ(library.program.types[id->second].kind, TypeKind::Modifier) << name; + for (const Case& common : + {Case{"at", "std::at"}, Case{"movement_mode", "std::movement_mode"}, + Case{"track", "std::track"}, Case{"shape", "std::any_shape"}}) { + const scena::dsl::FieldInfo* field = + library.program.find_field(id->second, common.parameter); + ASSERT_NE(field, nullptr) << name << "." << common.parameter; + ASSERT_NE(field->type, scena::dsl::kInvalidType) << name << "." << common.parameter; + EXPECT_EQ(library.program.types[field->type].name, common.type) + << name << "." << common.parameter; + } + } +} + +TEST(DslStdlibTest, TheMovementModifiersAreAssociatedWithTheirActor) { + // §7.3.12.3's example of an actor-associated modifier is + // `modifier vehicle.keep_lane()`, annotated "keep_lane() is defined in the + // domain model (see §8.9.16)" — §7.3 says outright how §8.9 is declared. + // Each goes on the most general actor that can run the actions it tunes. + Library library; + check_full_library(library); + const auto movable = library.program.types_by_name.find("std::movable_object"); + const auto vehicle = library.program.types_by_name.find("std::vehicle"); + ASSERT_NE(movable, library.program.types_by_name.end()); + ASSERT_NE(vehicle, library.program.types_by_name.end()); + struct Case { + const char* name; + const char* actor; + const char* field; + }; + for (const Case& expected : + {Case{"std::movable_object.position", "movable_object", "at_point"}, + Case{"std::movable_object.keep_position", "movable_object", nullptr}, + Case{"std::movable_object.speed", "movable_object", "faster_than"}, + Case{"std::movable_object.acceleration", "movable_object", "acceleration_range"}, + Case{"std::movable_object.lateral", "movable_object", "measure_by"}, + Case{"std::movable_object.yaw", "movable_object", "angle_range"}, + Case{"std::movable_object.orientation", "movable_object", "roll"}, + Case{"std::movable_object.along", "movable_object", "start_offset"}, + Case{"std::movable_object.along_trajectory", "movable_object", "trajectory"}, + Case{"std::movable_object.distance", "movable_object", "distance"}, + Case{"std::movable_object.physical_movement", "movable_object", "option"}, + Case{"std::movable_object.avoid_collisions", "movable_object", "avoid"}, + // §7.3.12.3 puts keep_lane on the vehicle by name; `lane` follows it. + Case{"std::vehicle.lane", "vehicle", "from"}, + Case{"std::vehicle.keep_lane", "vehicle", nullptr}}) { + const auto id = library.program.types_by_name.find(expected.name); + ASSERT_NE(id, library.program.types_by_name.end()) << expected.name; + const TypeInfo& modifier = library.program.types[id->second]; + EXPECT_EQ(modifier.kind, TypeKind::Modifier) << expected.name; + const auto actor = + std::string(expected.actor) == "vehicle" ? vehicle->second : movable->second; + EXPECT_EQ(modifier.actor_type, actor) << expected.name; + if (expected.field != nullptr) { + EXPECT_NE(library.program.find_field(id->second, expected.field), nullptr) + << expected.name << "." << expected.field; + } + } +} + +TEST(DslStdlibTest, ThreeModifiersAreUnassociatedBecauseTheStandardCollides) { + // §8.8 declares ACTIONS `movable_object.change_speed`, + // `movable_object.keep_speed` and `vehicle.change_lane`; §8.9 declares + // MODIFIERS of the same names for the same actors. A qualified behavior + // name identifies exactly one declaration (§7.2.2.2.5), so the language + // cannot hold both. §7.3.12.3's unassociated form is the only spelling + // that exists and does not collide. The defect is the standard's. + Library library; + check_full_library(library); + struct Case { + const char* modifier; + const char* action; + }; + for (const Case& expected : {Case{"std::change_speed", "std::movable_object.change_speed"}, + Case{"std::keep_speed", "std::movable_object.keep_speed"}, + Case{"std::change_lane", "std::vehicle.change_lane"}}) { + const TypeInfo* modifier = library.program.find(expected.modifier); + ASSERT_NE(modifier, nullptr) << expected.modifier; + EXPECT_EQ(modifier->kind, TypeKind::Modifier) << expected.modifier; + EXPECT_EQ(modifier->actor_type, scena::dsl::kInvalidType) + << expected.modifier << " must stay unassociated"; + const TypeInfo* action = library.program.find(expected.action); + ASSERT_NE(action, nullptr) << expected.action; + EXPECT_EQ(action->kind, TypeKind::Action) << expected.action; + } +} + +TEST(DslStdlibTest, TheScalarAndRangeParametersAreSeparateFields) { + // §8.9.1.4 pairs a scalar parameter with a range-typed counterpart: + // `speed` fixes one value for the whole action, `speed_range` lets it vary + // within the interval. "At most one of them is used within an invocation" + // constrains applications, not the declaration, so both are declared. + Library library; + check_full_library(library); + struct Case { + const char* modifier; + const char* scalar; + const char* range; + }; + for (const Case& expected : + {Case{"std::movable_object.speed", "speed", "speed_range"}, + Case{"std::change_speed", "speed", "speed_range"}, + Case{"std::movable_object.acceleration", "acceleration", "acceleration_range"}, + Case{"std::movable_object.lateral", "distance", "distance_range"}, + Case{"std::movable_object.yaw", "angle", "angle_range"}, + Case{"std::movable_object.position", "distance", "distance_range"}, + Case{"std::movable_object.position", "time", "time_range"}}) { + const auto id = library.program.types_by_name.find(expected.modifier); + ASSERT_NE(id, library.program.types_by_name.end()) << expected.modifier; + const scena::dsl::FieldInfo* scalar = + library.program.find_field(id->second, expected.scalar); + const scena::dsl::FieldInfo* ranged = + library.program.find_field(id->second, expected.range); + ASSERT_NE(scalar, nullptr) << expected.modifier << "." << expected.scalar; + ASSERT_NE(ranged, nullptr) << expected.modifier << "." << expected.range; + ASSERT_NE(scalar->type, scena::dsl::kInvalidType); + ASSERT_NE(ranged->type, scena::dsl::kInvalidType); + EXPECT_NE(scalar->type, ranged->type) << expected.modifier; + EXPECT_EQ(library.program.types[ranged->type].kind, TypeKind::Range) + << expected.modifier << "." << expected.range; + } +} + +TEST(DslStdlibTest, AssociationIsWhatMakesTheChapterDeclarable) { + // Corroboration for the association reading, and a guard against someone + // "simplifying" these to unassociated modifiers later: an unassociated + // `lane` would collide head-on with §8.12.10's `struct lane`, and an + // unassociated `speed` would shadow `stdtypes::speed` badly enough that + // `range of speed` stops naming a physical type. + Library library; + check_full_library(library); + const TypeInfo* lane = library.program.find("std::lane"); + ASSERT_NE(lane, nullptr); + EXPECT_EQ(lane->kind, TypeKind::Struct) << "std::lane must remain §8.12.10's road struct"; + EXPECT_EQ(library.program.find("std::speed"), nullptr) + << "the speed modifier is std::movable_object.speed, not std::speed"; + const TypeInfo* physical_speed = library.program.find("stdtypes::speed"); + ASSERT_NE(physical_speed, nullptr); + EXPECT_EQ(physical_speed->kind, TypeKind::Physical); +} + TEST(DslStdlibTest, CheckingTheLibraryIsDeterministic) { // Load time is inside the determinism contract: the same sources must give // the same program and the same diagnostics, in the same order.