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
87 changes: 84 additions & 3 deletions docs/architecture/ADR-0030-dsl-lowering-to-the-ir.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ SPDX-License-Identifier: Apache-2.0

- **Status:** Accepted
- **Date:** 2026-08-02
- **Sprint:** p8-s1 (#44)
- **Sprint:** p8-s1 (#44), extended in the same sprint's second half
- **Supersedes:** nothing. Builds on ADR-0028 (symbols and the type model),
ADR-0029 (the bundled standard library) and ADR-0010 (the entity taxonomy).

Expand Down Expand Up @@ -115,10 +115,83 @@ Anything that would need search is a diagnostic, not a silently-defaulted value
frontend takes for constructs it does not implement. A scenario that declares no
participant is a warning: the file is well-formed, it simply has nothing to run.

### 6. A behavior invocation denotes an IR action; the `do` directive denotes a storyboard

The runtime the two frontends share already knows what a speed transition is, so
lowering a §8.8 movement action decides only which IR action it *is*. Seven of
them have an unambiguous counterpart and lower:

| §8.8 action | IR action |
|---|---|
| `assign_speed` | `SpeedAction`, Step |
| `change_speed` | `SpeedAction` with the transition §8.8.2.18 asks for |
| `remain_stationary` | `SpeedAction` to zero, Step |
| `assign_position` | `TeleportAction` to a `WorldPosition` |
| `change_lane` | `LaneChangeAction` with a `RelativeTargetLane` |
| `change_space_gap` | `LongitudinalDistanceAction`, distance |
| `change_time_gap` | `LongitudinalDistanceAction`, time gap |

`move`, `drive` and `walk` are the generic actions: they carry no target of
their own and exist to be shaped by §8.9 modifiers (p8-s3). On their own they
say "keep doing what you are doing", which the runtime already does, so they
lower to nothing. Every other §8.8 action is reported by name, never silently
dropped.

Two readings are worth stating because the standard does not state them:

- **`dynamic_profile` names a shape, `rate_peak` a magnitude, and neither is a
duration.** With no peak rate there is no number to ramp over, so the change
is instantaneous — which is exactly what a Step shape means (§7.4.1.2).
`asap` is a Step from the other direction: §8.7 declares no performance
envelope, so "as soon as possible" is bounded by nothing.
- **`change_lane` needs an explicit `left` or `right`.** §8.8.3.14's `inside`,
`outside` and `same` need road geometry to say which way that is, and an
unstated side would have to be *chosen*. Choosing is what the determinism
contract forbids, so this is a diagnostic rather than a default.

The `do` directive becomes one Story, one Act, and one ManeuverGroup per phase —
the group is where an actor lives, so a group per phase keeps each invocation's
actor with its actions. `serial` chains a phase on its predecessor reaching
completeState, which is the trigger form §7.6.2.1.2's "starts when its
predecessor ends" already has in the runtime; `parallel` leaves the triggers
absent, so every phase starts with its Act (§7.6.1.1). A phase without a label
is named for its position, because the runtime addresses storyboard elements by
name path. Everything else about composition — `one_of`, nesting, `until`,
`wait`, `emit`, `call`, and an invocation's `duration` — is reported and belongs
to p8-s2.

### 7. A struct-valued argument names a declaration, and the `keep`s on it are the value

The DSL has no struct constructor: §7.2.2.6.7 declares list and range
constructors and nothing else. So `assign_position(position: start)` can only
name a declaration, and what makes `start` concrete is the same binding table
decision 3 already uses for an actor's own attributes. Nothing new is invented;
the rule is simply applied one level further out. A coordinate nothing
constrains is reported, not assumed.

### 8. The map file travels beside the IR, not inside it

§8.5.4's `map_file` is the DSL's counterpart to `RoadNetwork/LogicFile`, and it
gets the same treatment the XML frontend already gives that: a road-network path
is an input to the *host*, not kernel state, because the engine reaches roads
only through `IRoadQuery` (ADR-0003). `LowerResult` therefore mirrors
`xml::Document` — the IR plus the file reference — rather than growing a path
field on `ir::Scenario`.

Both spellings the standard prints are read and mean the same thing:
`map.set_map_file("m.xodr")` (Code 61) and `keep(my_map.map_file == "m.xodr")`
(Code 62). The first needs one resolution rule the checker did not have: **a
bare actor type name is a receiver in its own right**. `map` in Code 61 names
the actor *type* — the road network is a singleton no scenario declares a field
for — and without that rule the standard's own concrete-scenario example
(Code 6) is rejected. A declared field of the same name still wins, because the
receiver a reader means by a name is the declaration in front of them.

## Consequences

- `.osc` produces an `ir::Scenario` the existing engine accepts. Actions,
`set_map_file` and `scena-run`'s `.osc` support are the rest of p8-s1.
- `.osc` produces an `ir::Scenario` the existing engine accepts, and
`scena-run` runs one: the file's extension picks the frontend and everything
past the load sees only the IR.
- Lowering is inside the determinism contract, because load time is. It reads
ordered containers only, walks fields in declaration order, and does no
floating-point arithmetic of its own — the values it copies were folded once,
Expand All @@ -137,6 +210,14 @@ what executes.
fabricates numbers the standard does not state. "Unconstrained" is what §8.7
actually says, and the runtime already spells it as zero.

**Give `drive()` a default action so every scenario runs something.** Rejected:
the generic actions mean "unconstrained", and inventing a speed for them would
put a number in the trace that the scenario never stated.

**Put the road-network path in `ir::Scenario`.** Rejected: it would make the
kernel carry a host-side file reference, which is the coupling the gateway
exists to prevent, and the XML frontend already states the opposite rule.

**Solve constraints during lowering.** Rejected by ADR-0004: constraint solving
is post-v0.0.1, and half-solving would produce scenarios whose behaviour depends
on the solver's search order — the opposite of the determinism promise.
9 changes: 7 additions & 2 deletions docs/roadmap/coverage/osc-dsl-coverage.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,8 +50,13 @@ time is.
| Concrete value binding | §7.3.11 | In | In | p8-s1 | **Landed**: `keep(<field-path> == <constant>)` in either operand order, where the constant folds without a solver — that is what §6.3.1.2.1's "attribute-level concrete" means. §7.3.8.2 conditional inheritance is read the same way. Anything needing search is diagnosed, never approximated (ADR-0004) |
| Physical values in the IR | §7.3.4 | In | In | p8-s1 | **Landed**: values arrive already folded to their base unit, so lowering never converts and never re-applies the standard's printed factors a second time (ADR-0029) |
| Performance limits | §8.7 | n/a | Excl | p8-s1 | §8.7 declares no performance limits at all — the domain model has no counterpart to XML's `Performance`. The IR's zeros are the faithful lowering: the runtime reads a non-positive limit as unconstrained. No numbers are invented |
| §8.8 movement actions → IR actions | §8.8.2–§8.8.4 | In | In | p8-s1 | Planned (p8-s1 follow-up): the subset the action table below marks In |
| `set_map_file` → road backend | §8.12.2 | In | In | p8-s1 | Planned (p8-s1 follow-up) |
| §8.8 movement actions → IR actions | §8.8.2–§8.8.4 | In | In | p8-s1 | **Landed** (`dsl_lowering_test.cpp`): `assign_speed`, `change_speed`, `remain_stationary`, `assign_position`, `change_lane`, `change_space_gap`, `change_time_gap`. `dynamic_profile` names the shape and `rate_peak` the magnitude; with no peak rate there is no number to ramp over, so the change is a Step (§7.4.1.2). Every action with no counterpart is reported by name, never silently dropped |
| Generic actions (`move`, `drive`, `walk`) | §8.8.2.3, §8.8.3.1, §8.8.4.1 | In | In | p8-s1, p8-s3 | **Landed**: they carry no target of their own and exist to be shaped by §8.9 modifiers, so on their own they lower to nothing — "keep doing what you are doing" is what the runtime already does |
| Struct-valued action arguments | §7.2.2.6.7 | In | In | p8-s1 | **Landed**: the DSL declares list and range constructors and no struct constructor, so a struct-valued argument names a declaration and the `keep`s on it are the value (ADR-0030 §7). A coordinate nothing constrains is reported, not assumed |
| `do` directive → storyboard | §7.6.2.1 | In | Partial | p8-s1, p8-s2 | **Landed**: one Story, one Act, one ManeuverGroup per phase; `serial` chains a phase on its predecessor reaching completeState (§7.6.2.1.2), `parallel` leaves the triggers absent (§7.6.1.1). `one_of`, nesting, `until`, `wait`, `emit`, `call` and an invocation `duration` are reported and land in p8-s2 |
| `set_map_file` → road backend | §8.5.4, §8.12.2 | In | In | p8-s1 | **Landed** (`dsl_lowering_test.cpp`, `scena_run_test.cpp`): both spellings the standard prints — `map.set_map_file("m.xodr")` (Code 61) and `keep(my_map.map_file == "m.xodr")` (Code 62). The reference travels beside the IR, as `RoadNetwork/LogicFile` does, because a road-network path is a host input not kernel state (ADR-0003) |
| Actor type name as a modifier receiver | §7.3.12.4.1, §8.5.4 | In | n/a | p8-s1 | **Landed** (`dsl_types_test.cpp`): `map` in Code 61 names the actor *type* — the road network is a singleton no scenario declares a field for — so a bare actor type name is a receiver in its own right. A declared field of the same name still wins |
| `scena-run` runs `.osc` | — | n/a | In | p8-s1 | **Landed** (`scena_run_test.cpp`): the extension picks the frontend, `--entry` names the §7.7.2 entry point, `-I` adds an import search path. Same options, exit codes and trace format as XML |

## Language core (§7.2, §7.3)

Expand Down
4 changes: 2 additions & 2 deletions docs/user-guide/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,8 +56,8 @@ The user guide grows sprint by sprint along the
reach position), the traffic-signal conditions, the scalar-velocity model, the observation-sampling
contract, and the host interface (C++/C/Python).
- [`scena-run`](scena-run.md) — the headless CLI: options and exit codes, the
round-trip-exact trace format, replaying a host-controlled entity, and the
golden suite harness.
round-trip-exact trace format, replaying a host-controlled entity, running a
`.osc` file through the DSL frontend, and the golden suite harness.
- [`scena-check`](scena-check.md) — the OpenSCENARIO DSL checker: options and
exit codes, the diagnostic format, resolving imports and search paths,
checking from C and Python, and what "checked clean" does and does not cover.
Expand Down
31 changes: 31 additions & 0 deletions docs/user-guide/scena-run.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,40 @@ scena-run tests/golden/scenarios/gs1-cruise-baseline.xosc \
| `--map <file.xodr>` | road network, overriding the scenario's `RoadNetwork/LogicFile` |
| `--replay <entity>=<file>` | drive an entity from a recorded trace |
| `--select <alternative>` | choose a `one_of` alternative (DSL; p8-s2) |
| `--entry <scenario>` | the DSL scenario to run (`.osc` only) |
| `-I`, `--search-path <dir>` | where DSL imports are looked up (repeatable) |
| `--quiet` | do not print diagnostics |
| `-h`, `--help` | usage |

## Two frontends, one runtime

The file's extension picks the frontend: `.osc` is compiled by the
OpenSCENARIO DSL frontend, anything else by the OpenSCENARIO XML frontend.
Everything past the load — the engine, the gateway, the trace — sees only the
Scenario IR, so the options, the exit codes and the trace format are the same
either way.

```sh
scena-run cruise.osc --dt 0.01 --duration 12 --trace out/cruise.csv
```

A DSL file may declare several scenarios, and §7.7.2 leaves it to the
implementation which one runs. Scena runs the only one when there is only one
and reports the choice when there is not, because guessing would make the run
depend on declaration order:

```
error: the file declares more than one scenario; name the entry point (§7.7.2): demo::first demo::second
```

`--entry second` (or `--entry demo::second`) names it. `-I` adds a directory to
the import search path, exactly as it does for `scena-check`.

The road network works the same way in both languages: XML names it in
`RoadNetwork/LogicFile`, DSL in §8.5.4's `map_file` — written either as
`map.set_map_file("m.xodr")` or as `keep(my_map.map_file == "m.xodr")`. Both
are resolved relative to the scenario file, and `--map` overrides either.

Only whole steps run: `--duration 1 --dt 0.3` runs three steps, not three and a
third. A variable last step would make the tail of every trace depend on how the
duration happens to divide.
Expand Down
17 changes: 17 additions & 0 deletions frontends/dsl/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -242,3 +242,20 @@ expected. It also keeps the dependency list unchanged.
evaluation searches the use list after the current namespace (§7.7.4.2).
Before p8-s1 it searched only the current namespace, which silently made every
enum-valued `keep` look like one that needs a solver.
- **A movement action denotes an IR action, and the `do` directive a
storyboard.** Seven §8.8 actions have an unambiguous counterpart and lower;
the generic `move`/`drive`/`walk` carry no target of their own and lower to
nothing, because they exist to be shaped by §8.9 modifiers (p8-s3). Anything
with no counterpart is reported by name — never silently dropped. `serial`
chains a phase on its predecessor reaching completeState, `parallel` leaves
the triggers absent; the rest of composition is p8-s2.
- **A struct-valued argument names a declaration.** §7.2.2.6.7 declares list and
range constructors and no struct constructor, so `assign_position(position:
start)` reaches its numbers through the same `keep`s that make an actor
concrete.
- **The map file travels beside the IR.** §8.5.4's `map_file` is the DSL's
`RoadNetwork/LogicFile`, and like it, a road-network path is a host input
rather than kernel state — the engine reaches roads only through
`IRoadQuery`. `map.set_map_file("m.xodr")` needs one resolution rule the
checker did not have: a bare **actor type name** is a receiver in its own
right, because the map is a singleton no scenario declares a field for.
19 changes: 18 additions & 1 deletion frontends/dsl/include/scena/dsl/lower.h
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,23 @@ struct LowerOptions {
std::string entry_point;
};

/// What lowering produced: the IR plus the host-side references that are not
/// kernel state.
///
/// The shape mirrors `xml::Document` deliberately. A road-network file is an
/// input to the *host*, not to the engine — the engine reaches roads only
/// through `IRoadQuery` (ADR-0003) — so it travels beside the IR rather than
/// inside it, exactly as `xml::RoadNetwork` does.
struct LowerResult {
ir::Scenario scenario;
/// §8.5.4's `map_file`, verbatim and unresolved: the string the scenario
/// wrote, whether through `map.set_map_file("m.xodr")` (Code 61) or through
/// a `keep` on a declared `map` field (Code 62). Interpreting it relative to
/// the scenario file is the host's decision, the same rule the XML side
/// states for `LogicFile`.
std::string map_file;
};

/// The scenarios a root file offers as entry points, in declaration order.
///
/// What a CLI prints when the choice is ambiguous, and what an editor would
Expand All @@ -61,6 +78,6 @@ struct LowerOptions {
///
/// Returns Status::Ok when nothing was reported as an error.
[[nodiscard]] Status lower(const Program& program, const LoadResult& loaded,
const LowerOptions& options, ir::Scenario& out, DiagnosticSink& sink);
const LowerOptions& options, LowerResult& out, DiagnosticSink& sink);

} // namespace scena::dsl
Loading
Loading