Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
130 changes: 130 additions & 0 deletions docs/architecture/ADR-0032-dsl-movement-modifiers.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
<!--
SPDX-FileCopyrightText: 2026 Robomous
SPDX-License-Identifier: Apache-2.0
-->

# ADR-0032 — Lowering the §8.9 movement modifiers

- **Status:** Accepted
- **Date:** 2026-08-02
- **Sprint:** p8-s3 (#46)
- **Supersedes:** nothing. Builds on ADR-0030 (lowering to the IR) and ADR-0031
(the composition operators).

## Context

The DSL's generic actions — `drive()`, `move()`, `walk()` — carry no target of
their own. ADR-0030 lowered them to nothing and said why: they exist to be
*shaped* by §8.9's modifiers, and until those lower there is nothing to shape.
The standard's own style guide is explicit that this is the idiomatic form
("prefer using generic actions", §9.1.8), so nearly every scenario in the
specification is `drive()` plus modifiers.

A modifier is an equality constraint on the invoked behavior's parameters
(§7.3.12.4), and §8.9 gives seventeen of them plus four parameters common to
all. The question is what each one *denotes* in a runtime that was built for
OpenSCENARIO XML.

## Decision

### 1. Every modifier lands on an action the runtime already has

No modifier machinery is added to the runtime. Each one lowers to an IR action
P2 or P5 already implements — a `SpeedAction`, a `TeleportAction`, a
`LaneChangeAction`, a `LaneOffsetAction`, a `LongitudinalDistanceAction`,
a `LateralDistanceAction` — or it is reported. That is the same rule ADR-0031
applied to composition, and it is what keeps one runtime under two frontends
rather than two runtimes that resemble each other.

| §8.9 modifier | IR action |
|---|---|
| `speed` (absolute) | `SpeedAction` |
| `speed` (`faster_than`/`slower_than`/`same_as`, `factor`) | `SpeedAction` with a `RelativeTargetSpeed` |
| `change_speed` | `SpeedAction` relative to the actor's own speed |
| `position` at the start | `TeleportAction` to a `RelativeObjectPosition` |
| `position` over the phase | `LongitudinalDistanceAction` |
| `lateral` (with `side_of`) | `LateralDistanceAction` |
| `lateral` (without) | `LaneOffsetAction` |
| `lane` (number) | `LaneChangeAction` to an absolute lane |
| `lane` (`side_of` + `side`) | `LaneChangeAction` to a relative lane |
| `change_lane` | `LaneChangeAction` relative to the actor |
| `keep_lane` | continuous `LaneOffsetAction` at zero |

### 2. The `at` anchor decides *how*, and the phase duration is what it needs

§8.9.1.1.1's anchor takes `start`, `end` or `all`, and it is optional.

- **absent or `all`** — the value holds for the invocation. Realised by setting
it when the phase begins, which is a Step transition.
- **`start`** — the same thing, said explicitly.
- **`end`** — the value must be *reached* by the end of the phase, so it becomes
a transition spread over the phase's length.

That last case is why p8-s2's durations are load-bearing here: without a
concrete duration there is no interval to spread the change over. Rather than
invent one, lowering reports and sets the value at once — a fabricated duration
would put a number in the trace the scenario never stated.

`position` is the modifier where the anchor changes the *kind* of action, not
just its shape: at the start it is a placement (`TeleportAction`), over the
phase it is a gap to reach and hold (`LongitudinalDistanceAction`). Both
readings are in §8.9.2; the anchor is what chooses between them.

### 3. "Keep doing what you are doing" lowers to nothing

`keep_speed` (§8.9.6) and `keep_position` (§8.9.3) constrain the actor not to
change. The runtime already holds an entity's speed and its relative position
between actions, so the faithful lowering is *no action*: one that set the
current value would be a no-op that nonetheless appears in the trace and
competes for the same action domain (§7.5).

`keep_lane` (§8.9.16) is different and does produce an action, because holding a
lane is active work — a continuous `LaneOffsetAction` at zero is exactly the
runtime's way of saying it.

### 4. What is reported, and why

- **`acceleration` (§8.9.7)** shapes the acceleration of a movement the phase is
already performing. There is no acceleration-target action in the IR — the
coverage matrix defers §8.8's for the same reason — and on its own the
modifier states a rate with nothing to apply it to.
- **`along` and `along_trajectory` (§8.9.11–.12)** need a concrete route or
trajectory *value*. The DSL has no struct constructor (§7.2.2.6.7), so one can
only come from §8.12.2's `map.create_route(...)`, and the standard itself says
map methods may be external implementations (§7.3.7.4) — post-v0.0.1.
- **`distance` (§8.9.13)** bounds a phase by distance travelled rather than by
time, and ADR-0031 sequences phases by time.
- **`yaw`, `orientation`, `physical_movement`, `avoid_collisions`** were already
Post in the coverage matrix and stay there.

Each is reported by name with its section, never silently dropped.

## Consequences

- The idiomatic DSL scenario — `drive()` with modifiers — now produces a
runnable IR, which is what GS-12 and GS-13 will be written in.
- Modifier lowering reads the phase's duration, so it sits after ADR-0031's
time arithmetic and depends on it. That ordering is now a fact of the code.
- **A pre-existing checker gap became visible and was filed as #110.** §7.3.3
says an overloaded enum literal "will depend on the type requirements of the
place it is used in"; Scena reports the ambiguity before consulting them. `at`
(§8.9.19) and `route_overlap_kind` (§8.12) both declare `start` and `end`, so
the specification's own `at: start` is rejected while `at: at!start` is
accepted. It blocks nothing — the qualified spelling works — but a conforming
scenario copied out of the standard does not check, so it is tracked rather
than absorbed.

## Alternatives considered

**Give the runtime a modifier concept.** Rejected: modifiers are a DSL surface,
and a runtime that knew about them would be machinery only one frontend could
reach.

**Default an `at: end` modifier to some duration.** Rejected: it puts a number
in the trace the scenario never stated, which is the same failure mode ADR-0030
rejects for performance limits and ADR-0031 for `one_of` selection.

**Lower `keep_speed` to a speed action holding the current value.** Rejected: it
is a no-op that would still occupy the longitudinal action domain (§7.5) and
could supersede a running action — a behaviour change dressed up as a
constraint.
18 changes: 11 additions & 7 deletions docs/roadmap/coverage/osc-dsl-coverage.md
Original file line number Diff line number Diff line change
Expand Up @@ -212,13 +212,17 @@ envelopes belong to the logical level).

| Modifier | Section | Exec | Sprint | Notes |
|---|---|---|---|---|
| position() / keep_position() | §8.9.2–.3 | In | p8-s3 | ahead_of/behind + distance forms; `*_range`/at_point-with-projection variants Post |
| speed() / change_speed() / keep_speed() | §8.9.4–.6 | In | p8-s3 | Absolute + faster_than/slower_than/same_as relative forms |
| acceleration() | §8.9.7 | In | p8-s3 | Scalar form |
| lateral() | §8.9.8 | In | p8-s3 | t-axis offset; measure_by default |
| lane() / change_lane() / keep_lane() | §8.9.14–.16 | In | p8-s3 | `change_lane` without an explicit side is diagnosed, not randomized (determinism) |
| along() / along_trajectory() | §8.9.11–.12 | In | p8-s3 | Route/path/trajectory binding |
| distance() | §8.9.13 | In | p8-s3 | Traveled-distance bound |
| position() | §8.9.2 | In | p8-s3 | **Landed** (`dsl_lowering_test.cpp`): `ahead_of`/`behind` with a concrete distance. The anchor changes the *kind* of action — at the start a `TeleportAction` placement, over the phase a `LongitudinalDistanceAction` gap (ADR-0032). Point forms and `*_range` Post |
| keep_position() | §8.9.3 | In | p8-s3 | **Landed**: lowers to *no* action. The runtime already holds relative position between actions, so one that set the current value would be a no-op that still occupies the action domain (§7.5) |
| speed() / change_speed() | §8.9.4–.5 | In | p8-s3 | **Landed**: absolute, and the `faster_than`/`slower_than`/`same_as` relative forms with `factor` → `RelativeTargetSpeed`. `change_speed` is relative to the actor's own speed. Ranges are reported |
| keep_speed() | §8.9.6 | In | p8-s3 | **Landed**: lowers to no action, same reason as `keep_position` |
| acceleration() | §8.9.7 | Post | — | Reported: it shapes an acceleration the phase is already performing, and the IR has no acceleration-target action — the same reason §8.8's are deferred |
| lateral() | §8.9.8 | In | p8-s3 | **Landed**: `side_of` → `LateralDistanceAction`, otherwise a `LaneOffsetAction` from the lane centre. Positive offsets are to the left (§7.4.1.4), so `side: right` is negative |
| lane() / change_lane() / keep_lane() | §8.9.14–.16 | In | p8-s3 | **Landed**: a lane number → an absolute lane target, `side_of`+`side` → a relative one, `keep_lane` → a continuous zero `LaneOffsetAction`. `change_lane` without an explicit side is diagnosed, not chosen (determinism) |
| along() / along_trajectory() | §8.9.11–.12 | Post | — | Reported: both need a concrete route or trajectory value, and the DSL has no struct constructor (§7.2.2.6.7) — one can only come from §8.12.2's map methods, which the standard says may be external (§7.3.7.4) |
| distance() | §8.9.13 | Post | — | Reported: it bounds a phase by distance travelled, and phase sequencing is by time (ADR-0031) |
| `at` phase anchoring | §8.9.1.1.1, §8.9.19 | In | p8-s3 | **Landed**: absent/`all`/`start` set the value when the phase begins (a Step); `end` reaches it over the phase, which needs the phase length p8-s2's duration fixes. Without one it is reported, never invented |
| Overloaded enum literal resolved by context | §7.3.3 | **Gap** | — | §7.3.3 says the literal "will depend on the type requirements of the place it is used in"; Scena reports the ambiguity first, so the standard's own `at: start` is rejected while `at: at!start` works. Tracked as #110; blocks nothing |
| yaw() / orientation() | §8.9.9–.10 | Post | — | Orientation-target modifiers deferred (teleport orientation covers placements) |
| physical_movement() | §8.9.17 | Post | — | Single documented default profile in v0.0.1 |
| avoid_collisions() | §8.9.18 | Post | — | No collision-avoidance controller in v0.0.1 (engine executes what the scenario says; Collision condition detects) |
Expand Down
12 changes: 12 additions & 0 deletions frontends/dsl/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -260,6 +260,18 @@ expected. It also keeps the dependency list unchanged.
by label from `LowerOptions::alternative`, defaulting to the first: the engine
has no seed machinery, and a hidden input is what determinism forbids.
`wait elapsed(d)` lowers to nothing but the offset.
- **Every §8.9 modifier lands on an action the runtime already has** (ADR-0032).
`drive()` plus modifiers is the idiomatic scenario, and the modifiers are what
give it content: speed targets (absolute and relative), placements and gaps,
lane targets and offsets. Nothing modifier-shaped exists in the runtime.
- **The `at` anchor decides how, and needs the phase's length.** Absent, `all`
and `start` set the value when the phase begins; `end` reaches it over the
phase, which is what makes p8-s2's durations load-bearing. Without a duration
it is reported, never invented. `position` is the one where the anchor changes
the *kind* of action: a placement at the start, a gap to hold over the phase.
- **`keep_speed()` and `keep_position()` lower to nothing, on purpose.** The
runtime already holds both between actions, so an action setting the current
value would be a no-op that still occupies the action domain (§7.5).
- **`..` has to beat a float that starts with a dot.** §7.2.2.6.7 spells the
range constructor `[a '..' b]` while §7.2.1.5.2 makes a float's leading digits
optional, so `[2..4]` is a race the operator must win. It did not until p8-s2;
Expand Down
Loading
Loading