From 1ec9f07561a91835c7f1a920f6268f5ae5608192 Mon Sep 17 00:00:00 2001 From: Armando Anaya Date: Sat, 1 Aug 2026 21:48:29 -0700 Subject: [PATCH] =?UTF-8?q?feat(dsl):=20standard=20library=20=E2=80=94=20m?= =?UTF-8?q?ovement=20modifiers=20(p7-s5)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Translates §8.9 into bundled DSL source: the `any_shape`/`common_*_shape` hierarchy §8.9.1.2 prints as DSL, the seven §8.9.19–§8.9.25 enums, and the seventeen modifiers that tune a movement action. With this the whole §8 standard library is declared. Every modifier carries §8.9.1.1's four common parameters — `at`, `movement_mode`, `track`, `shape` — on the strength of "The following parameters are common to all domain model movement modifiers". Five usage signatures omit the `` placeholder the other twelve include; the general statement is the normative one, and accepting a parameter the standard permits beats rejecting a valid scenario. The modifiers are actor-associated, which §7.3.12.3 states outright: its example of an actor-associated modifier is `modifier vehicle.keep_lane()`, annotated "keep_lane() is defined in the domain model (see §8.9.16)". Each one goes on the most general actor that can run the actions it tunes — `movable_object`, which owns `move()` and parents both `vehicle` and `person`, or `vehicle` for the lane-related pair §7.3.12.3 places there by name. Association turns out to be what makes the chapter declarable at all. An unassociated `modifier lane` collides head-on with §8.12.10's `struct lane`, and an unassociated `modifier speed` shadows `stdtypes::speed` so thoroughly that `speed_range: range of speed` stops naming a physical type. Under the qualified names neither problem arises. A test pins this so the declarations are not later "simplified" back into a collision. Three modifiers must nonetheless be unassociated. §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 in the standard, not in the translation, and it is recorded in the worksheet, the coverage matrix and a test. 43f's forward-looking guard fired on this, exactly as intended, and is updated rather than deleted: it now pins that an action and a modifier may share a simple name, and points at the collision test for the case they cannot. Second facet added to #100 while probing this chapter: a modifier application inside a `with:` block is not validated at all — a nonsense modifier name is accepted silently. That is the larger half of the gap, since the `with:` block is where the domain model expects nearly every movement modifier to be applied. §8.9 is the surface that fix should be tested against, and p8-s3 (#46) needs it first. 1285 gtest (+7), 184 pytest. The library still checks with zero diagnostics of any severity. Refs #43 --- .../08-09-movement-modifiers.md | 123 ++++++++ docs/dev/stdlib-worksheets/README.md | 4 + docs/roadmap/coverage/osc-dsl-coverage.md | 2 +- frontends/dsl/README.md | 12 +- frontends/dsl/src/stdlib.cpp | 259 +++++++++++++++- frontends/dsl/tests/dsl_stdlib_test.cpp | 276 +++++++++++++++++- 6 files changed, 657 insertions(+), 19 deletions(-) create mode 100644 docs/dev/stdlib-worksheets/08-09-movement-modifiers.md 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.