diff --git a/docs/dev/stdlib-worksheets/08-12-02-map.md b/docs/dev/stdlib-worksheets/08-12-02-map.md new file mode 100644 index 0000000..577b145 --- /dev/null +++ b/docs/dev/stdlib-worksheets/08-12-02-map.md @@ -0,0 +1,108 @@ +# §8.12.2 — actor `map` + +Source: ASAM OpenSCENARIO DSL 2.2.0, `domain-model` §8.12.2 (Tables 186–214). +Landed in slice 43e. Target chunk: `kDomainMap` in `frontends/dsl/src/stdlib.cpp`. + +`map` is the top-level actor holding the abstract road network. §8.12.2 prints +its parameters as one table, then its 18 methods as `extend map:` **code blocks** +(unusually — most of §8 is tables only), then its 12 modifiers as tables. + +Everything the actor references — `route`, `junction`, `road`, `lane`, +`lane_section`, `crossing`, the point/path/trajectory structs and the road enums — +landed in 43c. The two traffic-light fields land with §8.15 in the same PR. + +## Fields (Table 187) + +| Field | Type | Mandatory | ⚠ | +| --- | --- | --- | --- | +| `map_file` | `string` | no | | +| `routes` | `list of route` | yes | | +| `junctions` | `list of junction` | yes | | +| `driving_rule` | `driving_rule` | yes | field name equals its type name — allowed, cf. `hitch_receiver.hitch_type` (§8.7.13) | +| `traffic_light_groups` | `list of traffic_light_group` | no | §8.15.4.2; §8.15.7 prints this pair as a separate `extend map:` block | +| `traffic_light_control` | `list of traffic_light_cycle` | no | §8.15.6.2; same | + +§8.15.7 spells the last two as an `extend map:` block rather than as rows of +Table 187. Since both chapters land in one slice they are declared inline on the +actor, which is the same program — §7.3.15 makes a type the union of its +declarations either way. + +## Methods (§8.12.2.1, prototypes printed as code) + +All are declared `is undefined`: they are queries a runtime answers, and the +library only has to give them a signature to check against. + +| § | Method | Signature | ⚠ | +| --- | --- | --- | --- | +| .1.1 | `odr_to_route_point` | `(road_id: string, lane_id: string, s: length, t: length) -> route_point` | `lane_id` is documented optional; the language has no optional marker (§7.3.11 covers it) | +| .1.2 | `xyz_to_route_point` | `(x: length, y: length, z: length) -> route_point` | | +| .1.3 | `route_point_to_xyz` | `(route_point: route_point) -> xyz_point` | parameter name equals its type name | +| .1.4 | `outer_side` | `() -> side_left_right` | | +| .1.5 | `inner_side` | `() -> side_left_right` | | +| .1.6 | `create_route` | `(routes: list of route, connect_points_by: connect_route_points, legal_route: bool) -> compound_route` | Code 75/76 call it with one argument; defaults are `waypoint` / `true` per Table 191 but the prototype declares none, so none is declared | +| .1.7 | `create_route_point` | `(route: route, s: length, t: length) -> route_point` | | +| .1.8 | `create_xyz_point` | `(x: length, y: length, z: length) -> xyz_point` | | +| .1.9 | `create_odr_point` | `(road_id: string, lane_id: string, s: length, t: length) -> odr_point` | | +| .1.10 | `create_path` | `(points: list of pose_3d, interpolation: path_interpolation) -> path` | | +| .1.11 | `create_path_odr_points` | `(points: list of odr_point, interpolation: path_interpolation, on_road_network: bool) -> path` | | +| .1.12 | `create_path_route_points` | `(points: list of route_point, interpolation: path_interpolation, on_road_network: bool) -> path` | | +| .1.13 | `create_trajectory` | `(points: list of pose_3d, time_stamps: list of time, interpolation: path_interpolation) -> trajectory` | prototype prints `list of time,interpolation` with no space — typography, not syntax | +| .1.14 | `create_trajectory_odr_points` | `(points: list of odr_point, time_stamps: list of time, interpolation: path_interpolation, on_road_network: bool) -> trajectory` | | +| .1.15 | `create_trajectory_route_points` | `(points: list of route_point, time_stamps: list of time, interpolation: path_interpolation, on_road_network: bool) -> trajectory` | Code 82 omits `on_road_network` | +| .1.16 | `resolve_relative_path` | `(relative_path: relative_path, reference: physical_object, transform: relative_transform) -> path` | parameter name equals its type name | +| .1.17 | `resolve_relative_trajectory` | `(relative_trajectory: relative_trajectory, reference: physical_object, transform: relative_transform) -> trajectory` | same | +| .1.18 | `get_map_file` | `() -> string` | | + +## Modifiers (§8.12.2.2, tables only) + +Declared in the §7.2.2.2.9 prefixed form `modifier map.:` — an actor's +modifier is never `modifier of map`, because §7.3.12.2's `of` names a +scenario or an action (43b finding). + +| § | Modifier | Fields | +| --- | --- | --- | +| .2.1 | `number_of_lanes` | `route: route`, `num_of_lanes: uint`, `lane_type: lane_type`, `lane_use: lane_use`, `directionality: directionality` | +| .2.2 | `routes_are_in_sequence` | `preceding: route`, `succeeding: route`, `road: road` | +| .2.3 | `roads_follow_in_junction` | `junction: junction`, `in_road: road`, `out_road: road`, `direction: junction_direction`, `clockwise_count: uint`, `number_of_roads: uint`, `in_lane: lane`, `out_lane: lane`, `junction_route: route`, `resulting_route: route` | +| .2.4 | `routes_overlap` | `route1: route`, `route2: route`, `overlap_kind: route_overlap_kind` | +| .2.5 | `lane_side` | `lane1: lane`, `side: side_left_right`, `lane2: lane`, `count: uint`, `lane_section: lane_section` | +| .2.6 | `compound_lane_side` | `lane1: compound_lane`, `side: side_left_right`, `lane2: compound_lane`, `count: uint`, `route: route` | +| .2.7 | `end_lane` | `lane: lane` | +| .2.8 | `start_lane` | `lane: lane` | +| .2.9 | `crossing_connects` | `crossing: crossing`, `start_lane: lane`, `end_lane: lane`, `start_s_coord: length`, `start_angle: angle` | +| .2.10 | `routes_are_opposite` | `route1: route`, `route2: route`, `containing_road: road`, `lateral_overlap: lateral_overlap_kind` | +| .2.11 | `set_map_file` | `file: string` | +| .2.12 | `set_traffic_lights_control_file` | `file: string` | + +## ⚠ Notes carried into the library + +1. **Three modifier field names collide with type names in the same + namespace** — `lane_type`, `lane_use`, `directionality`, `lane_section`, + `crossing`, `junction`, `route`, `road`, `lane`. A member name and a type name + live in different lookup spaces, so this checks cleanly; it is worth knowing + because it reads like a collision. Pinned by test. +2. **`map.end_lane` / `map.start_lane` each take one field named `lane` of type + `lane`** — the narrowest instance of the above. +3. **Code 78/79's `map.create_path_odr_points(..., smooth, true)` passes a bare + enum literal.** `smooth` is unique to `path_interpolation`, so no `type!literal` + prefix is needed here — unlike `driving`, which §7.3.3 requires be written + `lane_type!driving` (43c finding). +4. **Table 205's `clockwise_count` documents `1`=left … `4`=U-turn** for a + four-way junction. That is runtime semantics, not a type constraint; the field + is a plain `uint`. +5. **§8.12.2.1.6's syntax examples (Code 75/76) call `create_route` with a single + list argument.** The declared prototype has three parameters and prints no + defaults, so the library declares three. A caller supplying one argument is a + caller-side concern (§7.2.2.5.2 positional arguments), not a library one. +6. **These twelve modifiers cannot yet be applied — issue #100.** Writing + `my_map.lane_side(a, side_left_right!left, b, 1, ls)` reports "unknown + modifier 'lane_side'". The cause is in the resolver, not in the library: the + parser keeps the whole qualified behavior name, so the declaration + `modifier map.lane_side` creates a type named `map.lane_side`, while + `check_modifier_application` looks up the bare `lane_side`. Unassociated + modifiers apply fine. The defect predates this chapter — §8.7's + `stationary_object.location` and `vehicle.tow_trailer` have it too — and it is + what §8.9's movement modifiers (p8-s3, #46) will need fixed first. + `TheMapModifiersAreAssociatedWithTheMap` pins the declarations, which are + well-formed; nothing pins the broken application, because that behaviour is + not the one we want to keep. diff --git a/docs/dev/stdlib-worksheets/08-15-traffic-lights.md b/docs/dev/stdlib-worksheets/08-15-traffic-lights.md new file mode 100644 index 0000000..7bb5292 --- /dev/null +++ b/docs/dev/stdlib-worksheets/08-15-traffic-lights.md @@ -0,0 +1,95 @@ +# §8.15 — traffic lights + +Source: ASAM OpenSCENARIO DSL 2.2.0, `domain-model` §8.15 (Tables 301–343). +Landed in slice 43h. Target chunk: `kDomainTrafficLights` in +`frontends/dsl/src/stdlib.cpp`. + +§8.15 is three enums and a bulb struct (§8.15.2), a semantic-state enum +(§8.15.3), the traffic-light and group structs with their methods (§8.15.4), the +stop line (§8.15.5), phase and cycle (§8.15.6), the two `map` fields (§8.15.7), +the controller actor (§8.15.8) and seven actions (§8.15.9). Everything is +printed as tables except the four method prototypes. + +## Enums + +| § | Enum | Members | ⚠ | +| --- | --- | --- | --- | +| .2.1 | `bulb_icon` | 45 values, `unknown` first — see the chunk for the full list in table order | the comment column maps each to an OpenDRIVE signal type; not carried, it is documentation | +| .2.2 | `bulb_color` | `unknown, red, yellow, green, blue, white` | every member also exists in `color` (§8.7.15) → callers need `bulb_color!red` (§7.3.3) | +| .2.3 | `bulb_state` | `unknown, is_off, is_on, is_flashing` | | +| .3.1 | `semantic_traffic_light_state` | `off, stop, attention, caution, stop_attention, go, go_exclusive, non_functional` | `off` is not a reserved word (§7.2.1.5.1 makes keywords positional), so it needs no escaping | +| .5.1 | `stop_line_marking` | `none, solid, broken` | `none` also exists in `hitch_type` (§8.7.18) and `directionality` (§8.12.6) | + +## Structs + +| § | Struct | Members | ⚠ | +| --- | --- | --- | --- | +| .2.4 | `traffic_light_bulb` | `map_id: string`, `icon: bulb_icon`, `color: bulb_color`, `icon_positive: bool`, `state: bulb_state` | Table 308 lists the first four as parameters, Table 309 lists `state` as a state variable; both become fields (43b rule) | +| .4.1 | `traffic_light` | `map_id: string`, `bulbs: list of traffic_light_bulb`, `pose: pose_3d`, `height: length`, `width: length`, `group: traffic_light_group` | forward reference to `traffic_light_group`, declared below it | +| .4.2 | `traffic_light_group` | `map_id: string`, `bulbs: list of traffic_light_bulb`, `traffic_lights: list of traffic_light`, `cycle: traffic_light_cycle` | mutually recursive with `traffic_light`; `cycle` forward-references §8.15.6.2 | +| .5.2 | `traffic_light_stop_line` **inherits `route_element`** | `traffic_light_group: traffic_light_group`, `route: route`, `rightmost_lane: uint`, `leftmost_lane: uint`, `offset: length`, `secondary_stop_offset: length`, `primary_stop_line_marking: stop_line_marking`, `secondary_stop_line_marking: stop_line_marking` | two field names equal their type names | +| .6.1 | `traffic_light_phase` | `group: traffic_light_group`, `bulbs_state: list of traffic_light_bulb`, `duration: time` | | +| .6.2 | `traffic_light_cycle` | `phases: list of traffic_light_phase`, `synchronization_group_id: uint`, `start_offset: time` | | + +## Methods (§8.15.4.1.1, §8.15.4.2.1 — prototypes printed as code) + +| Owner | Method | Signature | +| --- | --- | --- | +| `traffic_light` | `state_equal` | `(bulbs: list of traffic_light_bulb) -> bool` | +| `traffic_light` | `semantic_state_to_state` | `(state: semantic_traffic_light_state) -> list of traffic_light_bulb` | +| `traffic_light` | `state_to_semantic_state` | `(bulbs: list of traffic_light_bulb) -> semantic_traffic_light_state` | +| `traffic_light_group` | `state_equal` | `(bulbs: list of traffic_light_bulb) -> bool` | + +⚠ §8.15.4.2.1's prototype for the **group's** `state_equal` prints +`extend traffic_light:`, not `extend traffic_light_group:`. The surrounding prose, +the section heading and Table 319 all say the method belongs to +`traffic_light_group`, so the prototype's receiver is a copy-paste slip and the +method is declared on the group. This is the one place in the chapter where the +printed code is not followed; see note 1 below. + +## Actor and actions + +`actor traffic_light_controller inherits osc_actor` (§8.15.8.1, Table 329). + +The seven §8.15.9 actions are declared on it. §8.8.1 defines only +`action_for_environment` and `action_for_movable_object` as intermediate bases, +so these inherit `osc_actor.osc_action` directly rather than inventing a third +intermediate. All are instantaneous except `play_cycles`; action ending is +runtime semantics and is not expressible in a declaration. + +| § | Action | Fields | +| --- | --- | --- | +| .9.1.1 | `set_bulb_state` | `traffic_light: traffic_light`, `bulb_color: bulb_color`, `bulb_kind: bulb_icon`, `bulb_state: bulb_state`, `sync: bool` | +| .9.2.1 | `set_state` | `traffic_light: traffic_light`, `state: list of bulb_state`, `sync: bool` | +| .9.3.1 | `set_semantic_state` | `traffic_light: traffic_light`, `state: semantic_traffic_light_state`, `sync: bool` | +| .9.4.1 | `set_group_bulb_state` | `traffic_light: traffic_light`, `bulb_color: bulb_color`, `bulb_kind: bulb_icon`, `bulb_state: bulb_state`, `sync: bool` ⚠ | +| .9.5.1 | `set_group_state` | `group: traffic_light_group`, `state: list of bulb_state`, `sync: bool` | +| .9.6.1 | `set_group_semantic_state` | `traffic_light_group: traffic_light_group`, `state: semantic_traffic_light_state`, `sync: bool` | +| .9.7.1 | `play_cycles` | `cycles: list of traffic_light_cycle` | + +## ⚠ Notes carried into the library + +1. **Table 337 (`set_group_bulb_state`) names its first parameter + `traffic_light` of type `traffic_light`, while its description says "The + traffic light group affected by the action".** The parameter table is the + normative surface and it is what a conforming scenario would be written + against, so the printed name and type are carried verbatim and the + inconsistency is recorded here. This follows the same rule as the §8.14.1.3 + conversion factors (ADR-0029): what the standard prints wins over what it + evidently meant. Contrast §8.15.4.2.1, where the *prose, the heading and the + table* agree against a single line of printed code — there the code loses. + The distinction is which surface the rest of the chapter corroborates. +2. **`set_group_state` names the parameter `group`, `set_group_semantic_state` + names it `traffic_light_group`.** Both carried as printed. +3. **`traffic_light` and `traffic_light_group` are mutually recursive** through + `traffic_light.group` and `traffic_light_group.traffic_lights`. The resolver's + declare pass registers every type before the link pass reads any field type, + so declaration order is free; they are declared in chapter order. +4. **Table 313's `state` for `traffic_light` does not exist.** A traffic light has + no state of its own — its state is the state of its bulbs, which is why + §8.15.4.1.1 provides `state_equal` and the two conversion methods instead. The + §8.15.10 examples read state only through those methods. +5. **§8.15.10's examples write `stop_line.road`,** but Table 323 declares the + field as `route`. The table is normative and the example is illustrative; the + field is `route`. An author following the example gets an "unknown member" + diagnostic, which is the correct outcome. diff --git a/docs/dev/stdlib-worksheets/README.md b/docs/dev/stdlib-worksheets/README.md new file mode 100644 index 0000000..6506ec6 --- /dev/null +++ b/docs/dev/stdlib-worksheets/README.md @@ -0,0 +1,33 @@ +# Standard-library translation worksheets + +Working notes for the bundled ASAM OpenSCENARIO DSL §8 standard library +(`frontends/dsl/src/stdlib.cpp`, ADR-0029). + +Most of §8 is printed as *parameter tables*, not as DSL code. Turning a chapter +into library source is therefore a translation, one table row at a time, and the +expensive part is reading the chapter rather than writing the declarations. A +worksheet captures that reading once: one line per declaration, in the order the +chapter prints them, with the resulting DSL spelling and a `⚠` note wherever the +printed text needs a judgement call. + +The worksheets are committed for two reasons: + +- they are the auditable record of the translation — a reviewer can check a + worksheet row against the specification without re-deriving the mapping from + the library source; and +- they are the recovery path. Re-reading a chapter costs far more than reading + its worksheet. + +They are developer notes, not user documentation: `docs/user-guide/` describes +what the frontend does, and `docs/roadmap/coverage/osc-dsl-coverage.md` records +what of the standard is covered. A worksheet only records how one chapter was +translated. + +| Worksheet | Chapter | Landed in | +| --- | --- | --- | +| [`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 | + +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 +the ADR and in `frontends/dsl/README.md`. diff --git a/docs/roadmap/coverage/osc-dsl-coverage.md b/docs/roadmap/coverage/osc-dsl-coverage.md index 5afa951..fbd90bd 100644 --- a/docs/roadmap/coverage/osc-dsl-coverage.md +++ b/docs/roadmap/coverage/osc-dsl-coverage.md @@ -110,9 +110,10 @@ included, so a conforming implementation and Scena agree. | `stdtypes` — string methods | §8.13 | In | p7-s5 | **Landed**: declared on the `string` primitive. §8.13 heads them under the types sub-module and then names the `std` namespace; declared under the section they appear in, and nothing observable turns on it — a method on a primitive is reached through a value, never through a namespace | | `std` — physical-object actors, structs and enums | §8.7 | In | p7-s5 | **Landed** (`dsl_stdlib_test.cpp`): the `osc_actor` → `physical_object` → `movable_object` → `traffic_participant` → `vehicle` → `trailer` chain with `stationary_object`, `person` and `animal`; `bounding_box`, `axle`, `hitch_receiver`, `hitch_coupler`; all 11 enums including the backward-compatibility spellings (`truck = heavy_truck`, `fire = fire_brigade`, …) sharing their replacement's value; the §8.7.6.1 measurement methods. §8.7.26 (traffic-participant groups) is non-normative and excluded | | `std` — road abstraction classes | §8.12.3–§8.12.41 | In | p7-s5 | **Landed** (`dsl_stdlib_test.cpp`): the `route` → `route_element` hierarchy with `road`, `lane_section`, `lane`, `crossing`, the four point structs, `path`/`trajectory` and their relative variants, `compound_route`/`compound_lane`, `junction`, and 15 enums. The road-dependent `physical_object` and `traffic_participant` methods arrive here as `extend` blocks, which is how the standard prints their prototypes | -| `std` — the `map` actor | §8.12.2 | In | p7-s5 | Lands with the next slice of #43; §8.12.2's method and modifier surface is larger than the rest of §8.12 combined | +| `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` — movement actions and modifiers, traffic lights, the `map` actor | §8.3, §8.5, §8.8.2–§8.9, §8.12.2, §8.15 | In | p7-s5 | Lands with the remaining slices of #43 | +| `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 | 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 602dfe5..d1dc870 100644 --- a/frontends/dsl/README.md +++ b/frontends/dsl/README.md @@ -168,7 +168,20 @@ expected. It also keeps the dependency list unchanged. - **An actor's modifier uses the prefixed form.** §8.7.4.1.1 writes `stationary_object.location()`, which is `modifier stationary_object.location` — not `modifier location of stationary_object`, because §7.3.12.2's `of` - names a scenario or an action. + 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. +- **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 + member `left` in both `side_left_right` and `junction_direction`. Only the + last of those needs qualifying, and §7.3.3 says how — `side_left_right!left`. + At library scale that is the normal case, not the exception. +- **Each chapter's translation has a worksheet.** `docs/dev/stdlib-worksheets/` + holds one file per translated chapter: every declaration with its resulting + DSL spelling and a note wherever the printed text needed a judgement call. + Reading a worksheet is much cheaper than re-reading the chapter. - **`check_source` / `check_file` are the entry points.** Loading and resolving in one call, with imports followed; the CLI and the bindings sit on them. The `LoadResult` owns the ASTs a `Program` points into, so it must outlive it. diff --git a/frontends/dsl/src/stdlib.cpp b/frontends/dsl/src/stdlib.cpp index e16105e..05cc2fb 100644 --- a/frontends/dsl/src/stdlib.cpp +++ b/frontends/dsl/src/stdlib.cpp @@ -698,14 +698,282 @@ action environment.assign_celestial_position inherits environment.action_for_env elevation: angle )OSC"; +// §8.15 the traffic lights: the bulb enums and struct, the semantic-state enum, +// the traffic-light and group structs with their methods, the stop line, the +// phase and cycle structs, the controller actor and its seven actions. +// Transcribed from the parameter tables of §8.15.2–§8.15.9; the four method +// prototypes are the chapter's only printed code. +// Translation worksheet: docs/dev/stdlib-worksheets/08-15-traffic-lights.md +constexpr std::string_view kDomainTrafficLights = R"OSC( +# --- §8.15.2.1 enum bulb_icon ------------------------------------------------ +# Table 302 pairs each icon with the OpenDRIVE signal type that draws it; that +# mapping is documentation and is not part of the declaration. +enum bulb_icon: [ + unknown, circle, pedestrian_walking, pedestrian_standing, tram, bus, + bicycle, horse_rider, person_bicycle, bicycle_left, bicycle_right, + arrow_left, arrow_right, arrow_straight, arrow_left_straight, + arrow_right_straight, arrow_diagonal_left, arrow_diagonal_right, + arrow_u_turn_left, arrow_u_turn_right, arrow_left_right, lane_arrow_down, + lane_arrow_down_right, lane_arrow_down_left, lane_cross, txt_walk, + txt_dont_walk, countdown, pt_horizontal_bar, pt_vertical_bar, pt_slash_bar, + pt_backslash_bar, pt_small_circle, pt_triangle, switch_x, switch_v_flipped, + switch_v_left, switch_v_right, switch_t, switch_a, switch_bar_v, + switch_bar_v_flipped, switch_bar_v_right, switch_bar_v_left, + switch_dotted_circle +] + +# --- §8.15.2.2 enum bulb_color ----------------------------------------------- +# Every member also names a member of `color` (§8.7.15), so §7.3.3 makes a +# scenario write `bulb_color!red` wherever both are in scope. +enum bulb_color: [unknown, red, yellow, green, blue, white] + +# --- §8.15.2.3 enum bulb_state ----------------------------------------------- +enum bulb_state: [unknown, is_off, is_on, is_flashing] + +# --- §8.15.2.4 struct traffic_light_bulb ------------------------------------- +# Table 308 calls the first four parameters and Table 309 calls `state` a state +# variable; the language has one kind of member, so both become fields. +struct traffic_light_bulb: + map_id: string + icon: bulb_icon + color: bulb_color + icon_positive: bool + state: bulb_state + +# --- §8.15.3.1 enum semantic_traffic_light_state ----------------------------- +enum semantic_traffic_light_state: [ + off, stop, attention, caution, stop_attention, go, go_exclusive, + non_functional +] + +# --- §8.15.4.1 struct traffic_light ------------------------------------------ +# A traffic light has no state member of its own: its state IS the state of its +# bulbs, which is what the three methods below read and convert. +struct traffic_light: + map_id: string + bulbs: list of traffic_light_bulb + pose: pose_3d + height: length + width: length + group: traffic_light_group + def state_equal(bulbs: list of traffic_light_bulb) -> bool is undefined + def semantic_state_to_state(state: semantic_traffic_light_state) -> list of traffic_light_bulb is undefined + def state_to_semantic_state(bulbs: list of traffic_light_bulb) -> semantic_traffic_light_state is undefined + +# --- §8.15.4.2 struct traffic_light_group ------------------------------------ +# §8.15.4.2.1's prototype prints `extend traffic_light:`, but its heading, its +# prose and Table 319 all place `state_equal` on the GROUP; the lone printed +# receiver is a slip and the method is declared here. +struct traffic_light_group: + map_id: string + bulbs: list of traffic_light_bulb + traffic_lights: list of traffic_light + cycle: traffic_light_cycle + def state_equal(bulbs: list of traffic_light_bulb) -> bool is undefined + +# --- §8.15.5.1 enum stop_line_marking ---------------------------------------- +enum stop_line_marking: [none, solid, broken] + +# --- §8.15.5.2 struct traffic_light_stop_line -------------------------------- +struct traffic_light_stop_line inherits route_element: + traffic_light_group: traffic_light_group + route: route + rightmost_lane: uint + leftmost_lane: uint + offset: length + secondary_stop_offset: length + primary_stop_line_marking: stop_line_marking + secondary_stop_line_marking: stop_line_marking + +# --- §8.15.6.1 struct traffic_light_phase ------------------------------------ +struct traffic_light_phase: + group: traffic_light_group + bulbs_state: list of traffic_light_bulb + duration: time + +# --- §8.15.6.2 struct traffic_light_cycle ------------------------------------ +struct traffic_light_cycle: + phases: list of traffic_light_phase + synchronization_group_id: uint + start_offset: time + +# --- §8.15.8.1 actor traffic_light_controller -------------------------------- +actor traffic_light_controller inherits osc_actor + +# --- §8.15.9 actions for controlling traffic lights -------------------------- +# §8.8.1 declares only `action_for_environment` and `action_for_movable_object` +# as intermediate bases, so these inherit `osc_action` directly rather than +# invent a third one. Every action but `play_cycles` is instantaneous; action +# ending is runtime semantics and has no place in a declaration. +action traffic_light_controller.set_bulb_state inherits osc_actor.osc_action: + traffic_light: traffic_light + bulb_color: bulb_color + bulb_kind: bulb_icon + bulb_state: bulb_state + sync: bool + +action traffic_light_controller.set_state inherits osc_actor.osc_action: + traffic_light: traffic_light + state: list of bulb_state + sync: bool + +action traffic_light_controller.set_semantic_state inherits osc_actor.osc_action: + traffic_light: traffic_light + state: semantic_traffic_light_state + sync: bool + +# Table 337 names this action's first parameter `traffic_light` of type +# `traffic_light` while its description says "the traffic light GROUP affected". +# The parameter table is the surface a conforming scenario is written against, +# so the printed name and type are carried verbatim — the same rule that keeps +# §8.14.1.3's rounded conversion factors (ADR-0029). +action traffic_light_controller.set_group_bulb_state inherits osc_actor.osc_action: + traffic_light: traffic_light + bulb_color: bulb_color + bulb_kind: bulb_icon + bulb_state: bulb_state + sync: bool + +action traffic_light_controller.set_group_state inherits osc_actor.osc_action: + group: traffic_light_group + state: list of bulb_state + sync: bool + +action traffic_light_controller.set_group_semantic_state inherits osc_actor.osc_action: + traffic_light_group: traffic_light_group + state: semantic_traffic_light_state + sync: bool + +action traffic_light_controller.play_cycles inherits osc_actor.osc_action: + cycles: list of traffic_light_cycle +)OSC"; + +// §8.12.2 the `map` actor: the top-level holder of the abstract road network, +// its 18 query methods and its 12 search-space modifiers. The methods are the +// chapter's printed `extend map:` prototypes; the fields and the modifiers come +// from the parameter tables of §8.12.2 and §8.12.2.2. +// Translation worksheet: docs/dev/stdlib-worksheets/08-12-02-map.md +constexpr std::string_view kDomainMap = R"OSC( +# --- §8.12.2 actor map ------------------------------------------------------- +# §8.15.7 prints `traffic_light_groups` and `traffic_light_control` as a +# separate `extend map:` block. §7.3.15 makes a type the union of its +# declarations, so declaring them inline here is the same program. +actor map inherits osc_actor: + map_file: string + routes: list of route + junctions: list of junction + driving_rule: driving_rule + traffic_light_groups: list of traffic_light_group + traffic_light_control: list of traffic_light_cycle + + # §8.12.2.1 methods. Each is a query a runtime answers; the library only + # has to give it a signature to check calls against. + def odr_to_route_point(road_id: string, lane_id: string, s: length, t: length) -> route_point is undefined + def xyz_to_route_point(x: length, y: length, z: length) -> route_point is undefined + def route_point_to_xyz(route_point: route_point) -> xyz_point is undefined + def outer_side() -> side_left_right is undefined + def inner_side() -> side_left_right is undefined + def create_route(routes: list of route, connect_points_by: connect_route_points, legal_route: bool) -> compound_route is undefined + def create_route_point(route: route, s: length, t: length) -> route_point is undefined + def create_xyz_point(x: length, y: length, z: length) -> xyz_point is undefined + def create_odr_point(road_id: string, lane_id: string, s: length, t: length) -> odr_point is undefined + def create_path(points: list of pose_3d, interpolation: path_interpolation) -> path is undefined + def create_path_odr_points(points: list of odr_point, interpolation: path_interpolation, on_road_network: bool) -> path is undefined + def create_path_route_points(points: list of route_point, interpolation: path_interpolation, on_road_network: bool) -> path is undefined + def create_trajectory(points: list of pose_3d, time_stamps: list of time, interpolation: path_interpolation) -> trajectory is undefined + def create_trajectory_odr_points(points: list of odr_point, time_stamps: list of time, interpolation: path_interpolation, on_road_network: bool) -> trajectory is undefined + def create_trajectory_route_points(points: list of route_point, time_stamps: list of time, interpolation: path_interpolation, on_road_network: bool) -> trajectory is undefined + def resolve_relative_path(relative_path: relative_path, reference: physical_object, transform: relative_transform) -> path is undefined + def resolve_relative_trajectory(relative_trajectory: relative_trajectory, reference: physical_object, transform: relative_transform) -> trajectory is undefined + def get_map_file() -> string is undefined + +# --- §8.12.2.2 modifiers ----------------------------------------------------- +# An actor's modifier takes the §7.2.2.2.9 PREFIXED form: §7.3.12.2's `of` +# names a scenario or an action, never an actor. +# +# Many of the field names below are also type names in this namespace +# (`route`, `road`, `lane`, `lane_section`, `crossing`, `junction`, +# `lane_type`, `lane_use`, `directionality`). Members and types are separate +# lookup spaces, so this checks cleanly; it is pinned by test because it reads +# like a collision. +modifier map.number_of_lanes: + route: route + num_of_lanes: uint + lane_type: lane_type + lane_use: lane_use + directionality: directionality + +modifier map.routes_are_in_sequence: + preceding: route + succeeding: route + road: road + +modifier map.roads_follow_in_junction: + junction: junction + in_road: road + out_road: road + direction: junction_direction + clockwise_count: uint + number_of_roads: uint + in_lane: lane + out_lane: lane + junction_route: route + resulting_route: route + +modifier map.routes_overlap: + route1: route + route2: route + overlap_kind: route_overlap_kind + +modifier map.lane_side: + lane1: lane + side: side_left_right + lane2: lane + count: uint + lane_section: lane_section + +modifier map.compound_lane_side: + lane1: compound_lane + side: side_left_right + lane2: compound_lane + count: uint + route: route + +modifier map.end_lane: + lane: lane + +modifier map.start_lane: + lane: lane + +modifier map.crossing_connects: + crossing: crossing + start_lane: lane + end_lane: lane + start_s_coord: length + start_angle: angle + +modifier map.routes_are_opposite: + route1: route + route2: route + containing_road: road + lateral_overlap: lateral_overlap_kind + +modifier map.set_map_file: + file: string + +modifier map.set_traffic_lights_control_file: + file: string +)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); + static const std::string source = std::string(kDomainEntities) + std::string(kDomainRoads) + + std::string(kDomainEnvironment) + + std::string(kDomainTrafficLights) + std::string(kDomainMap); return source; } diff --git a/frontends/dsl/tests/dsl_stdlib_test.cpp b/frontends/dsl/tests/dsl_stdlib_test.cpp index eb11d56..57b7bc4 100644 --- a/frontends/dsl/tests/dsl_stdlib_test.cpp +++ b/frontends/dsl/tests/dsl_stdlib_test.cpp @@ -867,6 +867,339 @@ TEST(DslStdlibTest, AnActionAndAStructMayShareASimpleName) { EXPECT_EQ(action->kind, TypeKind::Action); } +// --- §8.15 traffic lights --------------------------------------------------- + +TEST(DslStdlibTest, TheTrafficLightEnumsCarryTheirValues) { + Library library; + check_full_library(library); + struct Case { + const char* name; + std::size_t values; + const char* sample; + }; + for (const Case& expected : + {Case{"std::bulb_icon", 45, "arrow_u_turn_left"}, Case{"std::bulb_color", 6, "yellow"}, + Case{"std::bulb_state", 4, "is_flashing"}, + Case{"std::semantic_traffic_light_state", 8, "go_exclusive"}, + Case{"std::stop_line_marking", 3, "broken"}}) { + 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, TheTrafficLightStructsAreDeclaredWithTheirFields) { + Library library; + check_full_library(library); + struct Case { + const char* name; + const char* field; + }; + for (const Case& expected : + {Case{"std::traffic_light_bulb", "icon"}, + // Table 308 calls the first four parameters and Table 309 calls + // `state` a state variable; both are fields here. + Case{"std::traffic_light_bulb", "state"}, + Case{"std::traffic_light_bulb", "icon_positive"}, Case{"std::traffic_light", "bulbs"}, + Case{"std::traffic_light", "pose"}, Case{"std::traffic_light", "group"}, + Case{"std::traffic_light_group", "traffic_lights"}, + Case{"std::traffic_light_group", "cycle"}, + Case{"std::traffic_light_stop_line", "rightmost_lane"}, + Case{"std::traffic_light_stop_line", "secondary_stop_offset"}, + Case{"std::traffic_light_stop_line", "primary_stop_line_marking"}, + Case{"std::traffic_light_phase", "bulbs_state"}, + Case{"std::traffic_light_phase", "duration"}, Case{"std::traffic_light_cycle", "phases"}, + Case{"std::traffic_light_cycle", "synchronization_group_id"}, + Case{"std::traffic_light_cycle", "start_offset"}}) { + const auto id = library.program.types_by_name.find(expected.name); + ASSERT_NE(id, library.program.types_by_name.end()) << expected.name; + EXPECT_NE(library.program.find_field(id->second, expected.field), nullptr) + << expected.name << "." << expected.field; + } +} + +TEST(DslStdlibTest, AFieldMayShareItsNameWithItsType) { + // §8.15.5.2's stop line has a field `traffic_light_group` of type + // `traffic_light_group`, and §8.12.2's map has `driving_rule: driving_rule`. + // Members and types are separate lookup spaces, so this is legal — it is + // pinned because it reads like a collision and the standard does it often. + Library library; + check_full_library(library); + struct Case { + const char* owner; + const char* member; + const char* type; + }; + for (const Case& expected : + {Case{"std::traffic_light_stop_line", "traffic_light_group", "std::traffic_light_group"}, + Case{"std::traffic_light_stop_line", "route", "std::route"}, + Case{"std::map", "driving_rule", "std::driving_rule"}}) { + const auto id = library.program.types_by_name.find(expected.owner); + ASSERT_NE(id, library.program.types_by_name.end()) << expected.owner; + const scena::dsl::FieldInfo* field = + library.program.find_field(id->second, expected.member); + ASSERT_NE(field, nullptr) << expected.owner << "." << expected.member; + ASSERT_NE(field->type, scena::dsl::kInvalidType) << expected.member; + EXPECT_EQ(library.program.types[field->type].name, expected.type) << expected.member; + EXPECT_NE(library.program.find(expected.type), nullptr) << expected.type; + } +} + +TEST(DslStdlibTest, TheStopLineIsARouteElement) { + // Table 322 gives `traffic_light_stop_line` the parent `route_element`, so + // a stop line is something a movable object can be located on — which is + // what lets §8.15.10's examples measure distance to it along a route. + Library library; + check_full_library(library); + const auto stop_line = library.program.types_by_name.find("std::traffic_light_stop_line"); + const auto element = library.program.types_by_name.find("std::route_element"); + ASSERT_NE(stop_line, library.program.types_by_name.end()); + ASSERT_NE(element, library.program.types_by_name.end()); + EXPECT_TRUE(library.program.is_derived_from(stop_line->second, element->second)); +} + +TEST(DslStdlibTest, TheTrafficLightMethodsAreDeclaredWithTheirSignatures) { + // §8.15.4.2.1's prototype prints `extend traffic_light:` for the GROUP's + // `state_equal`; its heading, its prose and Table 319 all say the method + // belongs to the group, so the lone printed receiver loses. + Library library; + check_full_library(library); + struct Case { + const char* type; + const char* method; + std::size_t parameters; + const char* returns; + }; + for (const Case& expected : {Case{"std::traffic_light", "state_equal", 1, "bool"}, + Case{"std::traffic_light", "state_to_semantic_state", 1, + "std::semantic_traffic_light_state"}, + Case{"std::traffic_light_group", "state_equal", 1, "bool"}}) { + const auto id = library.program.types_by_name.find(expected.type); + ASSERT_NE(id, library.program.types_by_name.end()) << expected.type; + const scena::dsl::MethodInfo* method = + library.program.find_method(id->second, expected.method); + ASSERT_NE(method, nullptr) << expected.type << "." << expected.method; + EXPECT_EQ(method->parameters.size(), expected.parameters) << expected.method; + ASSERT_NE(method->return_type, scena::dsl::kInvalidType) << expected.method; + EXPECT_EQ(library.program.types[method->return_type].name, expected.returns) + << expected.method; + } + // `semantic_state_to_state` returns a list, so its return type is the + // structural aggregate the type table interns rather than a declared name. + const auto light = library.program.types_by_name.find("std::traffic_light"); + ASSERT_NE(light, library.program.types_by_name.end()); + const scena::dsl::MethodInfo* to_state = + library.program.find_method(light->second, "semantic_state_to_state"); + ASSERT_NE(to_state, nullptr); + ASSERT_NE(to_state->return_type, scena::dsl::kInvalidType); + EXPECT_EQ(library.program.types[to_state->return_type].kind, TypeKind::List); +} + +TEST(DslStdlibTest, TheTrafficLightActionsAreDeclaredOnTheController) { + // §8.15.8 gives traffic lights their own actor; §8.15.9's seven actions + // hang off it. §8.8.1 declares only `action_for_environment` and + // `action_for_movable_object` as intermediate bases, so these inherit + // `osc_action` directly rather than invent a third. + Library library; + check_full_library(library); + const auto controller = library.program.types_by_name.find("std::traffic_light_controller"); + const auto osc_actor = library.program.types_by_name.find("std::osc_actor"); + const auto osc_action = library.program.types_by_name.find("std::osc_actor.osc_action"); + ASSERT_NE(controller, library.program.types_by_name.end()); + ASSERT_NE(osc_actor, library.program.types_by_name.end()); + ASSERT_NE(osc_action, library.program.types_by_name.end()); + EXPECT_EQ(library.program.types[controller->second].kind, TypeKind::Actor); + EXPECT_TRUE(library.program.is_derived_from(controller->second, osc_actor->second)); + + struct Case { + const char* name; + const char* field; + }; + for (const Case& expected : + {Case{"std::traffic_light_controller.set_bulb_state", "bulb_kind"}, + Case{"std::traffic_light_controller.set_state", "state"}, + Case{"std::traffic_light_controller.set_semantic_state", "sync"}, + // Table 337 names this one's first parameter `traffic_light` of type + // `traffic_light` though its description says "group"; the parameter + // table is what a conforming scenario is written against. + Case{"std::traffic_light_controller.set_group_bulb_state", "traffic_light"}, + Case{"std::traffic_light_controller.set_group_state", "group"}, + Case{"std::traffic_light_controller.set_group_semantic_state", "traffic_light_group"}, + Case{"std::traffic_light_controller.play_cycles", "cycles"}}) { + 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, osc_action->second)) + << expected.name; + EXPECT_NE(library.program.find_field(id->second, expected.field), nullptr) + << expected.name << "." << expected.field; + } +} + +// --- §8.12.2 the map actor -------------------------------------------------- + +TEST(DslStdlibTest, TheMapActorHoldsTheRoadNetwork) { + // Table 187, plus the two fields §8.15.7 prints as a separate `extend map:` + // block. §7.3.15 makes a type the union of its declarations, so declaring + // them together is the same program. + Library library; + check_full_library(library); + const auto map = library.program.types_by_name.find("std::map"); + const auto osc_actor = library.program.types_by_name.find("std::osc_actor"); + ASSERT_NE(map, library.program.types_by_name.end()); + ASSERT_NE(osc_actor, library.program.types_by_name.end()); + EXPECT_EQ(library.program.types[map->second].kind, TypeKind::Actor); + EXPECT_TRUE(library.program.is_derived_from(map->second, osc_actor->second)); + for (const char* field : {"map_file", "routes", "junctions", "driving_rule", + "traffic_light_groups", "traffic_light_control"}) { + EXPECT_NE(library.program.find_field(map->second, field), nullptr) << field; + } +} + +TEST(DslStdlibTest, TheMapMethodsAreDeclaredWithTheirSignatures) { + // §8.12.2.1's eighteen prototypes. Unusually for §8 they are printed as + // `extend map:` code rather than as tables, so this is a transcription and + // the arities are the standard's own. + Library library; + check_full_library(library); + const auto map = library.program.types_by_name.find("std::map"); + ASSERT_NE(map, library.program.types_by_name.end()); + struct Case { + const char* method; + std::size_t parameters; + const char* returns; + }; + for (const Case& expected : + {Case{"odr_to_route_point", 4, "std::route_point"}, + Case{"xyz_to_route_point", 3, "std::route_point"}, + Case{"route_point_to_xyz", 1, "std::xyz_point"}, + Case{"outer_side", 0, "std::side_left_right"}, + Case{"inner_side", 0, "std::side_left_right"}, + Case{"create_route", 3, "std::compound_route"}, + Case{"create_route_point", 3, "std::route_point"}, + Case{"create_xyz_point", 3, "std::xyz_point"}, + Case{"create_odr_point", 4, "std::odr_point"}, Case{"create_path", 2, "std::path"}, + Case{"create_path_odr_points", 3, "std::path"}, + Case{"create_path_route_points", 3, "std::path"}, + Case{"create_trajectory", 3, "std::trajectory"}, + Case{"create_trajectory_odr_points", 4, "std::trajectory"}, + Case{"create_trajectory_route_points", 4, "std::trajectory"}, + Case{"resolve_relative_path", 3, "std::path"}, + Case{"resolve_relative_trajectory", 3, "std::trajectory"}, + Case{"get_map_file", 0, "string"}}) { + const scena::dsl::MethodInfo* method = + library.program.find_method(map->second, expected.method); + ASSERT_NE(method, nullptr) << expected.method; + EXPECT_EQ(method->parameters.size(), expected.parameters) << expected.method; + ASSERT_NE(method->return_type, scena::dsl::kInvalidType) << expected.method; + EXPECT_EQ(library.program.types[method->return_type].name, expected.returns) + << expected.method; + } +} + +TEST(DslStdlibTest, TheMapModifiersAreAssociatedWithTheMap) { + // §8.12.2.2's twelve search-space modifiers, in the §7.2.2.2.9 prefixed + // form. Applying one is issue #100 (the actor prefix stays in the declared + // name, so the application site cannot find it); the declarations + // themselves are well-formed, which is what this pins. + Library library; + check_full_library(library); + const auto map = library.program.types_by_name.find("std::map"); + ASSERT_NE(map, library.program.types_by_name.end()); + struct Case { + const char* name; + std::size_t parameters; + const char* field; + }; + for (const Case& expected : + {Case{"std::map.number_of_lanes", 5, "num_of_lanes"}, + Case{"std::map.routes_are_in_sequence", 3, "succeeding"}, + Case{"std::map.roads_follow_in_junction", 10, "clockwise_count"}, + Case{"std::map.routes_overlap", 3, "overlap_kind"}, + Case{"std::map.lane_side", 5, "count"}, Case{"std::map.compound_lane_side", 5, "lane1"}, + Case{"std::map.end_lane", 1, "lane"}, Case{"std::map.start_lane", 1, "lane"}, + Case{"std::map.crossing_connects", 5, "start_s_coord"}, + Case{"std::map.routes_are_opposite", 4, "lateral_overlap"}, + Case{"std::map.set_map_file", 1, "file"}, + Case{"std::map.set_traffic_lights_control_file", 1, "file"}}) { + 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; + EXPECT_EQ(modifier.actor_type, map->second) << expected.name; + EXPECT_EQ(modifier.field_order.size(), expected.parameters) << expected.name; + EXPECT_NE(library.program.find_field(id->second, expected.field), nullptr) + << expected.name << "." << expected.field; + } +} + +TEST(DslStdlibTest, AScenarioCanUseTheMapAndItsTrafficLights) { + // The end-to-end shape §8.15.10's examples are written in, minus the + // behaviour: declare the map and the lights, then constrain them. + DiagnosticSink sink; + LoadResult loaded; + Program program; + ASSERT_EQ(scena::dsl::check_source("import osc.standard.all\n" + "namespace demo use std, stdtypes\n" + "scenario crossing_light:\n" + " my_map: map\n" + " light: traffic_light\n" + " group: traffic_light_group\n" + " stop_line: traffic_light_stop_line\n" + " control: traffic_light_controller\n" + " keep(light.group == group)\n" + " keep(stop_line.traffic_light_group == group)\n" + " keep(stop_line.primary_stop_line_marking == solid)\n" + " keep(my_map.driving_rule == right_hand_traffic)\n", + "", LoadOptions{}, loaded, program, sink), + Status::Ok) + << (sink.diagnostics().empty() ? std::string() : sink.diagnostics().front().message); + EXPECT_FALSE(sink.has_errors()); +} + +TEST(DslStdlibTest, ASideLiteralNeedsItsEnumName) { + // `left` is a member of both `side_left_right` (§8.12.14) and + // `junction_direction` (§8.12.22). Adding the map made this reachable from + // ordinary scenario text, so §7.3.3's qualification is required — the same + // rule `lane_type!driving` already illustrates, on a word an author is far + // more likely to reach for. + DiagnosticSink sink; + LoadResult loaded; + Program program; + EXPECT_EQ(scena::dsl::check_source("import osc.standard.all\n" + "namespace demo use std, stdtypes\n" + "scenario turn:\n" + " where: side_left_right\n" + " keep(where == left)\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 turn:\n" + " where: side_left_right\n" + " keep(where == side_left_right!left)\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.