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.