Skip to content

feat(dsl): lower a checked DSL program to the Scenario IR (p8-s1) - #107

Merged
JArmandoAnaya merged 2 commits into
mainfrom
feat/p8-s1-dsl-lowering
Aug 2, 2026
Merged

feat(dsl): lower a checked DSL program to the Scenario IR (p8-s1)#107
JArmandoAnaya merged 2 commits into
mainfrom
feat/p8-s1-dsl-lowering

Conversation

@JArmandoAnaya

Copy link
Copy Markdown
Contributor

The first half of p8-s1: .osc in, ir::Scenario out. frontends/dsl/lower.h
turns a checked program into entities the existing engine accepts.

Lowering decides which DSL construct denotes which IR construct and nothing
else. Runtime semantics stay in the runtime, which both frontends share — the
architecture's rule, and the reason this is a small file.

Three decisions (ADR-0030)

The entry point is named, and a lone scenario names itself. §7.7.2 says
outright that selection "is defined by implementation", and that the latitude is
deliberate. LowerOptions::entry_point takes a qualified name (demo::overtake)
or the name as written. Empty means the root file's only scenario; a file
declaring several is an error that lists them. Guessing would make the run depend
on declaration order — exactly the hidden input the determinism contract exists
to remove. entry_points() returns the same list, in declaration order, because
that is what the file offers and a reader matches it against the file in front
of them.

A participant is a field deriving from std::physical_object. §8.7 roots its
actor hierarchy there. vehicle / person / stationary_object classify onto
ADR-0010's taxonomy; anything else deriving from physical_object stays an
unclassified participant. That last row is the interesting one: §8.7.10's
animal is a sibling actor, not a pedestrian category, and XML has nowhere to
put it. An identity and a control mode is all the runtime needs of it, and a
wrong classification would be worse than none.

Concrete means keep(field == constant). Either operand order, constant side
folding without a solver — that is what §6.3.1.2.1's "attribute-level concrete"
means, and it is the only shape whose meaning is unambiguous without search.
§7.3.8.2's conditional inheritance (inherits vehicle(vehicle_category == car))
is read the same way, since that is the spelling §8.7's own examples use.

Two things this deliberately does not do:

  • It never converts. §7.3.4 folding already happened during checking, so a
    physical value arrives in its base unit. Re-applying §8.14.1.3's printed
    factors a second time is precisely the bug ADR-0029 exists to prevent.
  • It never invents a number. §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, because the runtime already reads a non-positive limit
    as "unconstrained" (actor_max_speed). A category nothing fixes keeps
    ir::Vehicle's own default rather than a guess made here.

A pre-existing bug this surfaced

Constant evaluation resolved enum-name!member only in the current
namespace
, never through the use list (§7.7.4.2). A scenario in
namespace demo use std writing vehicle_category!bus could not fold it — and
because the checker classifies constraints by whether they fold, every
enum-valued keep written that way was reported as needing a solver. It was
invisible until something had to read those values.

Fixed in expression.cpp with the ordinary lookup order (explicit ::, then the
current namespace, then the use list), and pinned in dsl_expression_test.cpp.
This is why the vehicle-category tests here are meaningful rather than vacuous.

Tests

dsl_lowering_test.cpp (17): entry-point selection in all four shapes, the
taxonomy mapping including the unclassified case, geometry and category binding
from both operand orders, a value written in cm arriving in metres, the
performance-is-unconstrained rule, the empty-scenario warning, and that lowering
the same source twice gives bit-identical geometry — load time is inside the
determinism contract.

dsl_expression_test.cpp (+1) for the enum-lookup fix.

1339 gtest (+18), 194 pytest.

What is left of p8-s1

The §8.8 movement-action subset, set_map_file → road backend, and scena-run
accepting .osc. Split off because this half stands on its own and is worth
reviewing on its own: it is where the mapping decisions live, and the action
subset is mechanical once they are settled.

Refs #44

.osc in, entities out. Lowering decides which DSL construct denotes
which IR construct and nothing else — runtime semantics stay in the
runtime, which both frontends share.

Entry point (§7.7.2, implementation-defined by the standard's own words):
named by qualified name or as written; a root file declaring exactly one
scenario uses it without being asked, and a file declaring several is an
error listing them, because guessing would make the run depend on
declaration order.

Participants: every entry-scenario field whose type derives from
std::physical_object becomes one entity, in declaration order.
vehicle/person/stationary_object classify onto the p2-s1 taxonomy;
§8.7.10's animal has no counterpart there and stays unclassified rather
than being misfiled as a pedestrian.

Concrete values: keep(<field-path> == <constant>) in either operand
order, plus §7.3.8.2 conditional inheritance. Values arrive already
folded to base units, so lowering never converts — re-applying the
standard's printed factors a second time is the bug ADR-0029 exists to
prevent.

§8.7 declares no performance limits at all, so the IR's zeros are the
faithful lowering: the runtime already reads a non-positive limit as
unconstrained. No numbers are invented.

ADR-0030. 17 tests in dsl_lowering_test.cpp, including that lowering the
same source twice gives bit-identical geometry — load time is inside the
determinism contract.

Refs #44

Also fixes a pre-existing bug the lowering surfaced: constant evaluation
resolved `enum-name!member` only in the current namespace, never through
the use list (§7.7.4.2). A scenario in `namespace demo use std` writing
`vehicle_category!bus` therefore could not fold it, which silently made
every enum-valued `keep` look like one that needs a solver — including
in the checker's own constraint classification.

Refs #44
MSVC's C4456 is a warning-as-error in CI: the inner optional in the
conditional-inheritance branch reused the outer one's name.

Refs #44
@JArmandoAnaya
JArmandoAnaya merged commit b51de28 into main Aug 2, 2026
12 checks passed
@JArmandoAnaya
JArmandoAnaya deleted the feat/p8-s1-dsl-lowering branch August 2, 2026 12:19
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant