From 11dcc643e1659af68db34f4657865696465a02dc Mon Sep 17 00:00:00 2001 From: Armando Anaya Date: Sat, 1 Aug 2026 21:26:11 -0700 Subject: [PATCH] =?UTF-8?q?feat(dsl):=20standard=20library=20=E2=80=94=20m?= =?UTF-8?q?ovement=20actions=20(p7-s5)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Translates §8.8.2–§8.8.4 into bundled DSL source: fifteen actions for `movable_object`, thirteen for `vehicle`, `walk` for `person`, and the four enums they use. Also declares `action_for_vehicle` and `action_for_person`. §8.8.1 gives `osc_action` exactly two children, then §8.8.3 and §8.8.4 name these two as parents without any table declaring them; §8.8.1's prose supplies them — actions for actors below `movable_object` inherit `action_for_movable_object`. Fourteen actions would otherwise have no parent to name. Judgement calls, all recorded in the worksheet: - Mutually exclusive parameters are still plain fields. §8.8.2.4 says "use only one of the three"; `replay_path` marks both `absolute` and `relative` mandatory though only one can be given. The language has no choice group and §7.3.11 already lets a scenario leave a field unconstrained. - `change_position.target_xyz` is marked deprecated in the same row that declares it. It is declared: there is no deprecation marker in the language, and dropping a field the standard prints would reject a conforming scenario. - `change_lane.reference` documents `Default=it.actor`, which is not a constant expression, so no default is declared. - The `change_*`/`keep_*` gap pairs take different direction enums — `gap_direction` has six values, `road_distance_direction` two. Carried as printed and pinned, because it reads like an oversight. `change_speed`, `keep_speed` and `change_lane` are actions here and modifiers in §8.9. They do not collide: an action's name is a qualified behavior name (§7.2.2.2.5), so this chapter declares `movable_object.change_speed` while §8.9 will declare a plain `change_speed`. Pinned in both directions so the next slice cannot blur it. The worksheet README now records the reading technique that made this chapter tractable: §8.8 is 2190 lines, of which 725 are normative tables and the rest worked examples. Dropping the Examples subsections first is the difference between "too big for one sitting" and "one sitting". 1278 gtest (+8), 184 pytest. The library still checks with zero diagnostics of any severity. Refs #43 --- .../08-08-movement-actions.md | 115 +++++++++ docs/dev/stdlib-worksheets/README.md | 19 ++ docs/roadmap/coverage/osc-dsl-coverage.md | 3 +- frontends/dsl/src/stdlib.cpp | 186 ++++++++++++++- frontends/dsl/tests/dsl_stdlib_test.cpp | 223 ++++++++++++++++++ 5 files changed, 544 insertions(+), 2 deletions(-) create mode 100644 docs/dev/stdlib-worksheets/08-08-movement-actions.md diff --git a/docs/dev/stdlib-worksheets/08-08-movement-actions.md b/docs/dev/stdlib-worksheets/08-08-movement-actions.md new file mode 100644 index 0000000..effed30 --- /dev/null +++ b/docs/dev/stdlib-worksheets/08-08-movement-actions.md @@ -0,0 +1,115 @@ +# §8.8.2–§8.8.4 — movement actions + +Source: ASAM OpenSCENARIO DSL 2.2.0, `domain-model` §8.8.2–§8.8.4 +(Tables 88–146). Landed in slice 43f. Target chunk: `kDomainMovementActions` in +`frontends/dsl/src/stdlib.cpp`. + +Twenty-nine actions in three families, four enums, and the two intermediate +bases the chapter's "Parents" rows name but §8.8.1 does not declare. Every +action is printed as a "Basic information" table (parents, controlled states, +action ending) plus, where it takes arguments, a parameter table. Controlled +states and action ending are runtime semantics with no place in a declaration; +only the parents and the parameters translate. + +## The intermediate bases + +§8.8.1 declares `osc_action` with exactly two children, `action_for_environment` +and `action_for_movable_object` (both landed in 43d). §8.8.3's and §8.8.4's +"Parents" rows then name `action_for_vehicle` and `action_for_person`, which no +table declares. §8.8.1's prose supplies them: "Actions for actors that are +children of `movable_object`, like `vehicle` or `person`, inherit from +`action_for_movable_object`." + +``` +action vehicle.action_for_vehicle inherits movable_object.action_for_movable_object +action person.action_for_person inherits movable_object.action_for_movable_object +``` + +⚠ These two are the only declarations in the chapter not backed by a table of +their own. They are named as parents thirteen and one times respectively, so +leaving them out would make those thirteen actions unparentable. + +## §8.8.2 actions for movable object (parent `action_for_movable_object`) + +| § | Action | Fields | ⚠ | +| --- | --- | --- | --- | +| .3 | `move` | — | generic; §8.8.3's `drive` and §8.8.4's `walk` are its per-actor counterparts | +| .4 | `assign_position` | `position: position_3d`, `route_point: route_point`, `odr_point: odr_point` | "Use only one of the three"; all three are `Mandatory: no` | +| .5 | `assign_orientation` | `orientation: orientation_3d` | | +| .6 | `assign_speed` | `speed: speed` | | +| .7 | `assign_acceleration` | `acceleration: acceleration` | | +| .8 | `replay_path` | `absolute: path`, `relative: relative_path`, `reference: physical_object`, `transform: relative_transform`, `start_offset: length`, `end_offset: length` | `absolute` and `relative` are both `Mandatory: yes` yet mutually exclusive | +| .9 | `replay_trajectory` | same six, with `trajectory`/`relative_trajectory` | | +| .10 | `remain_stationary` | — | | +| .11 | `change_position` | `target_position: position`, `target_st: route_point`, `target_odr: odr_point`, `target_xyz: position_3d`, `interpolation: path_interpolation`, `on_road_network: bool` | `target_xyz` is marked deprecated in favour of `target_position`; carried, see note 3 | +| .12 | `change_speed` | `target: speed`, `rate_profile: dynamic_profile`, `rate_peak: acceleration` | | +| .13 | `keep_speed` | — | | +| .14 | `change_acceleration` | `target: acceleration`, `rate_profile: dynamic_profile`, `rate_peak: jerk` | | +| .15 | `keep_acceleration` | — | | +| .16 | `follow_path` | as `replay_path` | the target-behaviour counterpart of `replay_path` | +| .17 | `follow_trajectory` | as `replay_trajectory` | | +| .18 | enum `dynamic_profile` | `none, constant, smooth, asap` | `none` also in `hitch_type`, `directionality`, `stop_line_marking` | + +## §8.8.3 actions for vehicle (parent `action_for_vehicle`) + +| § | Action | Fields | +| --- | --- | --- | +| .1 | `drive` | — | +| .2 | `follow_lane` | `offset: length`, `rate_profile: dynamic_profile`, `rate_peak: speed`, `target: lane` | +| .3 | `change_lane` | `num_of_lanes: uint`, `side: lane_change_side`, `reference: physical_object`, `offset: length`, `rate_profile: dynamic_profile`, `rate_peak: speed`, `target: lane` | +| .4 | `change_time_gap` | `target: time`, `direction: gap_direction`, `reference: physical_object` | +| .5 | `keep_time_gap` | `reference: physical_object`, `direction: road_distance_direction` | +| .6 | `change_space_gap` | `target: length`, `direction: gap_direction`, `reference: physical_object` | +| .7 | `keep_space_gap` | `reference: physical_object`, `direction: road_distance_direction` | +| .8 | `change_time_headway` | `target: time`, `direction: headway_direction`, `reference: physical_object` | +| .9 | `keep_time_headway` | `reference: physical_object` | +| .10 | `change_space_headway` | `target: length`, `direction: headway_direction`, `reference: physical_object` | +| .11 | `keep_space_headway` | `reference: physical_object` | +| .12 | `connect_trailer` | `trailer: trailer` | +| .13 | `disconnect_trailer` | — | +| .14 | enum `lane_change_side` | `left, right, inside, outside, same` | +| .15 | enum `gap_direction` | `ahead, behind, left, right, inside, outside` | +| .16 | enum `headway_direction` | `ahead, behind` | + +Note the asymmetry the standard chose deliberately: the `change_*` actions take +a `gap_direction` (six values, longitudinal *and* lateral), while the `keep_*` +actions take a `road_distance_direction` (§8.7.22, just `longitudinal` and +`lateral`). Carried as printed. + +## §8.8.4 actions for person (parent `action_for_person`) + +| § | Action | Fields | +| --- | --- | --- | +| .1 | `walk` | — | + +§8.8.4's prose says a `person` **or an `animal`** walks, but the parent it names +is `action_for_person` and no `action_for_animal` exists. Declared on `person` +as printed; `animal` reaches `move` through `action_for_movable_object`. + +## ⚠ Notes carried into the library + +1. **Mutually exclusive parameters are still just fields.** `assign_position` + says "use only one of the three"; `replay_path` marks both `absolute` and + `relative` mandatory although only one can be given. The language has no + choice-group construct, and §7.3.11 lets a scenario leave a field + unconstrained, so all of them are plain fields. This is the same rule that + made every table row a field in 43b. +2. **`left`, `right`, `inside` and `outside` now live in four enums** + (`side_left_right`, `lane_change_side`, `gap_direction`, and + `junction_direction` for `left`/`right`). §7.3.3's `lane_change_side!left` + becomes unavoidable in ordinary scenario text. Worth a pinning test, since + §8.9's modifiers add more of the same. +3. **`change_position.target_xyz` is marked deprecated** in favour of + `target_position`, in the same table that declares it. It is declared: the + language has no deprecation marker, and dropping a field the standard prints + would reject a conforming scenario. A diagnostic for it belongs to a + deprecation pass, not to the library. +4. **`change_lane.reference` documents `Default=it.actor`.** A default that + names the invoking actor is not a constant expression, so no default is + declared — the field is left unconstrained, which is what §7.3.11 already + means. +5. **Three names appear as both an action here and a modifier in §8.9** + (`change_speed`, `keep_speed`, `change_lane`). They do not collide: an + action's name is a qualified behavior name (§7.2.2.2.5), so this chapter + declares `movable_object.change_speed` while §8.9 declares a plain + `change_speed`. The 43g slice must keep that distinction. diff --git a/docs/dev/stdlib-worksheets/README.md b/docs/dev/stdlib-worksheets/README.md index 6506ec6..d18ad0a 100644 --- a/docs/dev/stdlib-worksheets/README.md +++ b/docs/dev/stdlib-worksheets/README.md @@ -27,6 +27,25 @@ 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 | + +## Reading a chapter without reading all of it + +Most §8 chapters spend the majority of their lines on worked examples, which are +illustrative rather than normative and are not what a translation needs. §8.8 is +2190 lines; its normative tables are 725 of them. Dropping the `Examples` +subsections first makes the difference between "too big for one sitting" and +"one sitting": + +```sh +sed 's/\\//g' domain-model.md | awk 'NR>=2930 && NR<=5120' \ + | awk '/^##### .* Examples/{skip=1} /^#### /{skip=0} !skip' +``` + +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 +the first one. Chapters translated before the worksheet discipline existed (§8.7, §8.10, §8.11, §8.12.3–§8.12.41, §8.13, §8.14) have no worksheet; their findings are recorded in diff --git a/docs/roadmap/coverage/osc-dsl-coverage.md b/docs/roadmap/coverage/osc-dsl-coverage.md index fbd90bd..5f9a864 100644 --- a/docs/roadmap/coverage/osc-dsl-coverage.md +++ b/docs/roadmap/coverage/osc-dsl-coverage.md @@ -113,7 +113,8 @@ included, so a conforming implementation and Scena agree. | `std` — the `map` actor | §8.12.2 | In | p7-s5 | **Landed** (`dsl_stdlib_test.cpp`): the top-level road-network actor with its six fields, its 18 conversion and creation methods, and its 12 search-space modifiers. *Applying* an actor-associated modifier is issue #100 — the parser keeps the actor prefix in the declared name, so the application site cannot find it; the declarations are well-formed and are pinned. Worksheet: `docs/dev/stdlib-worksheets/08-12-02-map.md` | | `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 and modifiers | §8.3, §8.5, §8.8.2–§8.9 | In | p7-s5 | Lands with the remaining slices of #43 | +| `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 | 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/src/stdlib.cpp b/frontends/dsl/src/stdlib.cpp index 05cc2fb..e254965 100644 --- a/frontends/dsl/src/stdlib.cpp +++ b/frontends/dsl/src/stdlib.cpp @@ -965,6 +965,189 @@ modifier map.set_traffic_lights_control_file: file: string )OSC"; +// §8.8.2–§8.8.4 the movement actions: fifteen for `movable_object`, thirteen +// for `vehicle` and one for `person`, plus the four enums they use and the two +// intermediate bases §8.8.1 names only in prose. Each action's table gives it a +// parent, its controlled states and how it ends; only the parent and the +// parameters translate, the other two being runtime semantics. +// Translation worksheet: docs/dev/stdlib-worksheets/08-08-movement-actions.md +constexpr std::string_view kDomainMovementActions = R"OSC( +# --- §8.8.2/§8.8.3/§8.8.4 the intermediate action bases ---------------------- +# §8.8.1 declares `osc_action` with exactly two children. §8.8.3's and §8.8.4's +# "Parents" rows then name `action_for_vehicle` and `action_for_person`, which +# no table declares; §8.8.1's prose supplies them — "Actions for actors that +# are children of `movable_object`, like `vehicle` or `person`, inherit from +# `action_for_movable_object`". +action vehicle.action_for_vehicle inherits movable_object.action_for_movable_object + +action person.action_for_person inherits movable_object.action_for_movable_object + +# --- §8.8.2.18 enum dynamic_profile ------------------------------------------ +enum dynamic_profile: [none, constant, smooth, asap] + +# --- §8.8.2 actions for movable object --------------------------------------- +# The tables give each action a parent, its controlled states and how it ends. +# Only the parent and the parameters translate: the other two are runtime +# semantics with no place in a declaration. +action movable_object.move inherits movable_object.action_for_movable_object + +# §8.8.2.4: "Use only one of the three possible arguments" — the language has no +# choice group, and §7.3.11 already lets a scenario leave a field unconstrained. +action movable_object.assign_position inherits movable_object.action_for_movable_object: + position: position_3d + route_point: route_point + odr_point: odr_point + +action movable_object.assign_orientation inherits movable_object.action_for_movable_object: + orientation: orientation_3d + +action movable_object.assign_speed inherits movable_object.action_for_movable_object: + speed: speed + +action movable_object.assign_acceleration inherits movable_object.action_for_movable_object: + acceleration: acceleration + +action movable_object.replay_path inherits movable_object.action_for_movable_object: + absolute: path + relative: relative_path + reference: physical_object + transform: relative_transform + start_offset: length + end_offset: length + +action movable_object.replay_trajectory inherits movable_object.action_for_movable_object: + absolute: trajectory + relative: relative_trajectory + reference: physical_object + transform: relative_transform + start_offset: length + end_offset: length + +action movable_object.remain_stationary inherits movable_object.action_for_movable_object + +# `target_xyz` is marked deprecated in the same table that declares it, in +# favour of `target_position`. It is declared anyway: the language has no +# deprecation marker, and dropping a field the standard prints would reject a +# conforming scenario. +action movable_object.change_position inherits movable_object.action_for_movable_object: + target_position: position + target_st: route_point + target_odr: odr_point + target_xyz: position_3d + interpolation: path_interpolation + on_road_network: bool + +action movable_object.change_speed inherits movable_object.action_for_movable_object: + target: speed + rate_profile: dynamic_profile + rate_peak: acceleration + +action movable_object.keep_speed inherits movable_object.action_for_movable_object + +action movable_object.change_acceleration inherits movable_object.action_for_movable_object: + target: acceleration + rate_profile: dynamic_profile + rate_peak: jerk + +action movable_object.keep_acceleration inherits movable_object.action_for_movable_object + +# §8.8.2.16/§8.8.2.17 are the target-behaviour counterparts of replay_path and +# replay_trajectory (§8.8.2.1 vs §8.8.2.2): same parameters, different promise +# about the actor's dynamic limits. +action movable_object.follow_path inherits movable_object.action_for_movable_object: + absolute: path + relative: relative_path + reference: physical_object + transform: relative_transform + start_offset: length + end_offset: length + +action movable_object.follow_trajectory inherits movable_object.action_for_movable_object: + absolute: trajectory + relative: relative_trajectory + reference: physical_object + transform: relative_transform + start_offset: length + end_offset: length + +# --- §8.8.3.14 enum lane_change_side ----------------------------------------- +enum lane_change_side: [left, right, inside, outside, same] + +# --- §8.8.3.15 enum gap_direction -------------------------------------------- +enum gap_direction: [ahead, behind, left, right, inside, outside] + +# --- §8.8.3.16 enum headway_direction ---------------------------------------- +enum headway_direction: [ahead, behind] + +# --- §8.8.3 actions for vehicle ---------------------------------------------- +action vehicle.drive inherits vehicle.action_for_vehicle + +action vehicle.follow_lane inherits vehicle.action_for_vehicle: + offset: length + rate_profile: dynamic_profile + rate_peak: speed + target: lane + +# `reference` documents `Default=it.actor`; a default naming the invoking actor +# is not a constant expression, so the field is simply left unconstrained. +action vehicle.change_lane inherits vehicle.action_for_vehicle: + num_of_lanes: uint + side: lane_change_side + reference: physical_object + offset: length + rate_profile: dynamic_profile + rate_peak: speed + target: lane + +# The change_/keep_ pairs below are deliberately asymmetric: a `change_*` takes +# a `gap_direction` (six values, longitudinal and lateral), a `keep_*` takes a +# `road_distance_direction` (§8.7.22, just longitudinal and lateral). +action vehicle.change_time_gap inherits vehicle.action_for_vehicle: + target: time + direction: gap_direction + reference: physical_object + +action vehicle.keep_time_gap inherits vehicle.action_for_vehicle: + reference: physical_object + direction: road_distance_direction + +action vehicle.change_space_gap inherits vehicle.action_for_vehicle: + target: length + direction: gap_direction + reference: physical_object + +action vehicle.keep_space_gap inherits vehicle.action_for_vehicle: + reference: physical_object + direction: road_distance_direction + +action vehicle.change_time_headway inherits vehicle.action_for_vehicle: + target: time + direction: headway_direction + reference: physical_object + +action vehicle.keep_time_headway inherits vehicle.action_for_vehicle: + reference: physical_object + +action vehicle.change_space_headway inherits vehicle.action_for_vehicle: + target: length + direction: headway_direction + reference: physical_object + +action vehicle.keep_space_headway inherits vehicle.action_for_vehicle: + reference: physical_object + +action vehicle.connect_trailer inherits vehicle.action_for_vehicle: + trailer: trailer + +action vehicle.disconnect_trailer inherits vehicle.action_for_vehicle + +# --- §8.8.4 actions for person ----------------------------------------------- +# §8.8.4's prose says a person OR an animal walks, but the parent it names is +# `action_for_person` and no `action_for_animal` exists. Declared on `person` as +# printed; an animal reaches `move` through `action_for_movable_object`. +action person.walk inherits person.action_for_person +)OSC"; + const std::string& types_module() { static const std::string source = std::string(kTypesScalars) + std::string(kTypesCompound); return source; @@ -973,7 +1156,8 @@ const std::string& types_module() { 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(kDomainTrafficLights) + std::string(kDomainMap) + + std::string(kDomainMovementActions); return source; } diff --git a/frontends/dsl/tests/dsl_stdlib_test.cpp b/frontends/dsl/tests/dsl_stdlib_test.cpp index 57b7bc4..ca0f41a 100644 --- a/frontends/dsl/tests/dsl_stdlib_test.cpp +++ b/frontends/dsl/tests/dsl_stdlib_test.cpp @@ -1200,6 +1200,229 @@ TEST(DslStdlibTest, ASideLiteralNeedsItsEnumName) { : qualified.diagnostics().front().message); } +// --- §8.8.2–§8.8.4 movement actions ----------------------------------------- + +TEST(DslStdlibTest, TheMovementActionBasesFollowTheActorHierarchy) { + // §8.8.1 declares `osc_action` with exactly two children. §8.8.3 and §8.8.4 + // then name `action_for_vehicle` and `action_for_person` as parents without + // a table declaring either; §8.8.1's prose supplies them — actions for + // actors below `movable_object` inherit `action_for_movable_object`. + Library library; + check_full_library(library); + const auto movable = + library.program.types_by_name.find("std::movable_object.action_for_movable_object"); + ASSERT_NE(movable, library.program.types_by_name.end()); + for (const char* derived : + {"std::vehicle.action_for_vehicle", "std::person.action_for_person"}) { + const auto id = library.program.types_by_name.find(derived); + ASSERT_NE(id, library.program.types_by_name.end()) << derived; + EXPECT_EQ(library.program.types[id->second].kind, TypeKind::Action) << derived; + EXPECT_TRUE(library.program.is_derived_from(id->second, movable->second)) << derived; + } +} + +TEST(DslStdlibTest, TheMovableObjectActionsAreDeclaredWithTheirParameters) { + Library library; + check_full_library(library); + const auto base = + library.program.types_by_name.find("std::movable_object.action_for_movable_object"); + ASSERT_NE(base, library.program.types_by_name.end()); + struct Case { + const char* name; + std::size_t parameters; + const char* field; + }; + for (const Case& expected : {Case{"std::movable_object.move", 0, nullptr}, + // §8.8.2.4 says "use only one of the three"; the language has no + // choice group, so all three are plain fields (§7.3.11). + Case{"std::movable_object.assign_position", 3, "route_point"}, + Case{"std::movable_object.assign_orientation", 1, "orientation"}, + Case{"std::movable_object.assign_speed", 1, "speed"}, + Case{"std::movable_object.assign_acceleration", 1, "acceleration"}, + Case{"std::movable_object.replay_path", 6, "start_offset"}, + Case{"std::movable_object.replay_trajectory", 6, "transform"}, + Case{"std::movable_object.remain_stationary", 0, nullptr}, + Case{"std::movable_object.change_position", 6, "on_road_network"}, + Case{"std::movable_object.change_speed", 3, "rate_peak"}, + Case{"std::movable_object.keep_speed", 0, nullptr}, + Case{"std::movable_object.change_acceleration", 3, "rate_profile"}, + Case{"std::movable_object.keep_acceleration", 0, nullptr}, + Case{"std::movable_object.follow_path", 6, "absolute"}, + Case{"std::movable_object.follow_trajectory", 6, "relative"}}) { + const auto id = library.program.types_by_name.find(expected.name); + ASSERT_NE(id, library.program.types_by_name.end()) << expected.name; + EXPECT_EQ(library.program.types[id->second].kind, TypeKind::Action) << expected.name; + EXPECT_TRUE(library.program.is_derived_from(id->second, base->second)) << expected.name; + EXPECT_EQ(library.program.types[id->second].field_order.size(), expected.parameters) + << expected.name; + if (expected.field != nullptr) { + EXPECT_NE(library.program.find_field(id->second, expected.field), nullptr) + << expected.name << "." << expected.field; + } + } +} + +TEST(DslStdlibTest, TheVehicleAndPersonActionsAreDeclaredWithTheirParameters) { + Library library; + check_full_library(library); + const auto vehicle_base = library.program.types_by_name.find("std::vehicle.action_for_vehicle"); + const auto person_base = library.program.types_by_name.find("std::person.action_for_person"); + ASSERT_NE(vehicle_base, library.program.types_by_name.end()); + ASSERT_NE(person_base, library.program.types_by_name.end()); + struct Case { + const char* name; + const char* base; + std::size_t parameters; + const char* field; + }; + for (const Case& expected : + {Case{"std::vehicle.drive", "vehicle", 0, nullptr}, + Case{"std::vehicle.follow_lane", "vehicle", 4, "rate_peak"}, + Case{"std::vehicle.change_lane", "vehicle", 7, "num_of_lanes"}, + Case{"std::vehicle.change_time_gap", "vehicle", 3, "direction"}, + Case{"std::vehicle.keep_time_gap", "vehicle", 2, "reference"}, + Case{"std::vehicle.change_space_gap", "vehicle", 3, "target"}, + Case{"std::vehicle.keep_space_gap", "vehicle", 2, "direction"}, + Case{"std::vehicle.change_time_headway", "vehicle", 3, "direction"}, + Case{"std::vehicle.keep_time_headway", "vehicle", 1, "reference"}, + Case{"std::vehicle.change_space_headway", "vehicle", 3, "target"}, + Case{"std::vehicle.keep_space_headway", "vehicle", 1, "reference"}, + Case{"std::vehicle.connect_trailer", "vehicle", 1, "trailer"}, + Case{"std::vehicle.disconnect_trailer", "vehicle", 0, nullptr}, + Case{"std::person.walk", "person", 0, nullptr}}) { + const auto id = library.program.types_by_name.find(expected.name); + ASSERT_NE(id, library.program.types_by_name.end()) << expected.name; + EXPECT_EQ(library.program.types[id->second].kind, TypeKind::Action) << expected.name; + const auto base = + std::string(expected.base) == "vehicle" ? vehicle_base->second : person_base->second; + EXPECT_TRUE(library.program.is_derived_from(id->second, base)) << expected.name; + EXPECT_EQ(library.program.types[id->second].field_order.size(), expected.parameters) + << expected.name; + if (expected.field != nullptr) { + EXPECT_NE(library.program.find_field(id->second, expected.field), nullptr) + << expected.name << "." << expected.field; + } + } +} + +TEST(DslStdlibTest, TheChangeAndKeepActionsTakeDifferentDirectionEnums) { + // Deliberate asymmetry in the standard: a `change_*` gap action takes a + // six-valued `gap_direction` (§8.8.3.15), while its `keep_*` counterpart + // takes the two-valued `road_distance_direction` (§8.7.22). Carried as + // printed, and pinned because it looks like an oversight. + Library library; + check_full_library(library); + struct Case { + const char* action; + const char* type; + }; + for (const Case& expected : + {Case{"std::vehicle.change_time_gap", "std::gap_direction"}, + Case{"std::vehicle.change_space_gap", "std::gap_direction"}, + Case{"std::vehicle.keep_time_gap", "std::road_distance_direction"}, + Case{"std::vehicle.keep_space_gap", "std::road_distance_direction"}, + Case{"std::vehicle.change_time_headway", "std::headway_direction"}, + Case{"std::vehicle.change_space_headway", "std::headway_direction"}}) { + const auto id = library.program.types_by_name.find(expected.action); + ASSERT_NE(id, library.program.types_by_name.end()) << expected.action; + const scena::dsl::FieldInfo* field = library.program.find_field(id->second, "direction"); + ASSERT_NE(field, nullptr) << expected.action << ".direction"; + ASSERT_NE(field->type, scena::dsl::kInvalidType) << expected.action; + EXPECT_EQ(library.program.types[field->type].name, expected.type) << expected.action; + } +} + +TEST(DslStdlibTest, TheMovementEnumsCarryTheirValues) { + Library library; + check_full_library(library); + struct Case { + const char* name; + std::size_t values; + const char* sample; + }; + for (const Case& expected : + {Case{"std::dynamic_profile", 4, "asap"}, Case{"std::lane_change_side", 5, "same"}, + Case{"std::gap_direction", 6, "behind"}, Case{"std::headway_direction", 2, "ahead"}}) { + 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, ADeprecatedParameterIsStillDeclared) { + // §8.8.2.11's Table 103 declares `target_xyz` and marks it deprecated in + // favour of `target_position`, in the same row. The language has no + // deprecation marker, and dropping a field the standard prints would + // reject a conforming scenario, so both exist. Reporting the deprecation + // is a job for a later pass, not for the library. + Library library; + check_full_library(library); + const auto id = library.program.types_by_name.find("std::movable_object.change_position"); + ASSERT_NE(id, library.program.types_by_name.end()); + EXPECT_NE(library.program.find_field(id->second, "target_xyz"), nullptr); + EXPECT_NE(library.program.find_field(id->second, "target_position"), nullptr); +} + +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. + 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"; + } +} + +TEST(DslStdlibTest, ADirectionLiteralNeedsItsEnumName) { + // §8.8.3's three direction enums overlap heavily with each other and with + // §8.12's `side_left_right`: `left` is now a member of four enums and + // `ahead` of two. §7.3.3's qualification is the normal case at this scale. + DiagnosticSink sink; + LoadResult loaded; + Program program; + EXPECT_EQ(scena::dsl::check_source("import osc.standard.all\n" + "namespace demo use std, stdtypes\n" + "scenario gap:\n" + " where: gap_direction\n" + " keep(where == ahead)\n", + "", LoadOptions{}, loaded, program, sink), + Status::ValidationError); + bool explained = false; + for (const scena::Diagnostic& diagnostic : sink.diagnostics()) { + explained = explained || diagnostic.message.find("more than one enum") != std::string::npos; + } + EXPECT_TRUE(explained); + + DiagnosticSink qualified; + LoadResult qualified_loaded; + Program qualified_program; + EXPECT_EQ(scena::dsl::check_source("import osc.standard.all\n" + "namespace demo use std, stdtypes\n" + "scenario gap:\n" + " where: gap_direction\n" + " keep(where == gap_direction!ahead)\n", + "", LoadOptions{}, qualified_loaded, qualified_program, + qualified), + Status::Ok) + << (qualified.diagnostics().empty() ? std::string() + : qualified.diagnostics().front().message); +} + 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.