Grammar
A program is four files: the robot (.core), the world (.world), the task (.task), and optionally the evaluation (.eval). Each file holds exactly one declaration of its kind. The files are given to the compiler on the command line, and the extension says which form a file is. There is no project manifest: the target, the component libraries, and the output directory are compile-time settings, so they are command-line options, not something the author writes next to the program.
The grammar fixes the statement forms and nothing else. Component types, world entity types, task node types, and check names are ordinary names that later phases resolve against the standard vocabulary, the component libraries, and the target. A file that names a component or an action the compiler has never seen is syntactically valid; whether the name exists is decided by name resolution, not here. Adding vocabulary never changes the grammar.
The normative grammar is src/mobiledsl/frontend/grammar/robotdsl.lark, read by Lark with the LALR algorithm and a contextual lexer. The test suite builds every start rule in strict mode, so no two terminals can match the same text. When this page and the file disagree, the file is right and this page has a bug.
Common forms
Every declaration is a keyword, a name, and a braced body. Inside a body, items have one of two shapes:
Type name(parameter = value, ...) a named item of some type
keyword(parameter = value, ...) a setting with no nameArguments are always named. Items may be separated by commas, by line breaks, or by both, and a trailing comma is allowed. Comments are // to end of line and /* ... */; block comments do not nest. Line endings may be LF or CRLF.
Values
| Value | Form | Example |
|---|---|---|
| string | double quotes, JSON escapes | "mecanum_black", "a \"quoted\" word" |
| number | optional sign, decimal, optional exponent | 2, -0.0216, 1.5e-1 |
| boolean | true, false | is_static = true |
| tuple | two or more numbers in parentheses | (0, 0, -9.81), (2.5, 0, 0, 0, 0, 0, 1) |
| reference | @namespace/name | @core/wheel_lf, @world/goal, @task/drone_demo |
Numbers carry no unit; the parameter name does, by suffix: timeout_s, distance_m, angle_rad. A tuple's meaning (a pose of seven numbers, a size of three, a glue of six or seven) is decided by the parameter that takes it, not by the grammar. The reference namespaces are core, world, and task; any other namespace is a diagnostic, but the tree is still built. Identifiers are [A-Za-z_][A-Za-z0-9_]*.
CoreDSL (.core)
robot_decl: "Robot" NAME "{" intent? component* "}"
intent: "intent" STRING
component: NAME NAME paramsThe first name of a component is its type, the second its instance name. intent is a sentence for people and for the tools that generate programs; it comes before the components.
Robot drone_mecanum {
intent "Drone mecanum mobile base with four black mecanum wheels and a top lidar"
Mecanum_four_wheel chassis(ref = "tslot_1515_drone")
Mecanum wheel_lf(ref = "mecanum_black", glue = (0.1275, 0.1494, -0.0216, 0, 1, 0))
Lidar top_lidar(ref = "lidar_ld06", glue = (0, 0.0001, 0.0344, 0, 0, -1))
}WorldDSL (.world)
world_decl: "World" NAME "{" world_item* "}"
world_item: physics | spawn | scenario | goal
physics: "physics" params
spawn: "spawn" params
scenario: "Scenario" NAME params "{" entity* "}"
entity: NAME NAME params
goal: "Goal" NAME paramsphysics and spawn are settings of the world and have no name. A scenario groups the entities of one environment; each entity has a type (for example Obstacle, SupportSurface, Marker) and a name that tasks and checks refer to as @world/<name>. A goal is a named location.
World demo {
physics(gravity = (0, 0, -9.81))
spawn(robot = @core/drone_mecanum, pose = (0, 0, 0.05, 0, 0, 0, 1))
Scenario room(ref = "empty", ground_plane = true) {
Obstacle box(type = "box", pos = (1.2, 0, 0.25, 0, 0, 0, 1), size = (0.3, 0.3, 0.5), is_static = true)
}
Goal goal(pose = (2.5, 0, 0, 0, 0, 0, 1))
}TaskDSL (.task)
task_decl: "Task" NAME params? "{" node* "}"
node: NAME params block?
| NAME block
block: "{" node* "}"A node is a type followed by arguments, a block of children, or both. Which types take children (Sequence, Fallback, Retry, ...) and which are actions (Navigate, Spin, Wait, ...) is vocabulary, not grammar. A leaf keeps its parentheses even when it has no arguments, Stop(), so that two nodes in a row never read as one.
The task vocabulary names what the robot must accomplish. It does not name how a target accomplishes it: there is no node for a navigation stack's planner, controller, or costmap, and no syntax to select one. Those choices belong to the target and its configuration.
Task drone_demo(timeout_s = 120) {
Retry(count = 2) {
Navigate(goal = @world/goal)
}
Sequence {
Spin(angle_rad = 1.57),
Wait(duration_s = 2),
Stop()
}
}EvaluateDSL (.eval)
eval_decl: "Evaluate" NAME "{" check* "}"
check: NAME paramsEvaluate demo_eval {
Check_clearance(target = @world/goal, min = 0.3)
Check_vel(target = @core/wheel_lf, min = 0, max = 60)
Check_time(target = @task/drone_demo, max = 300)
}Accepted and rejected
Accepted:
| Source | Why it is interesting |
|---|---|
Robot r {} | the smallest declaration of each kind is valid |
Robot r { Hovercraft body(ref = "x") } | unknown vocabulary is a name-resolution question |
Task t { Dock(station = @world/dock) } | so are actions the compiler has never seen |
Robot r { A a(ref = "1"), B b(ref = "2"), } | commas are optional, a trailing one is fine |
Task t { Sequence { } Stop() Retry(count = 2) { Stop() } } | block only, arguments only, both |
Task t { Spin(angle_rad = -1.5e-1) } | signs and exponents |
// line /* block */ | both comment styles |
Rejected, with the diagnostic it produces:
| Source | Diagnostic |
|---|---|
World w {} in a .core file | syntax.unexpected_token: unexpected 'World'; expected 'Robot' |
Robot a {} Robot b {} | syntax.unexpected_token: unexpected 'Robot'; expected end of file |
Lidar l("lidar_ld06") | syntax.unexpected_token: expected one of ')', a name — arguments are named |
Stop Wait(duration_s = 1) | syntax.unexpected_token: unexpected 'Wait'; expected one of '(', '{' |
Goal g(pose = (1)) | syntax.unexpected_token: unexpected ')'; expected ',' — a tuple has two or more numbers |
Robot r {} # comment | syntax.unexpected_character: # comments are gone |
/* a /* b */ c */ | syntax.unexpected_token at c — block comments do not nest |
Robot r { | syntax.unexpected_eof: expected one of 'intent', '}', a name |
Navigate(goal = @wrold/goal) | syntax.unknown_reference_namespace |
Lidar l(ref = "a", ref = "b") | syntax.duplicate_parameter, pointing at both occurrences |
The diagnostics are specified in Diagnostics.
Changes from the previous compiler
The previous compiler's four grammars are the starting point. What changed, and why:
- Component types, task node types, and check names were grammar keywords (21, 15, and 9 of them). They are names now, so a new sensor, action, or check is a vocabulary entry, not a grammar change, and the task language is no longer tied to one navigation stack's node set.
- Poses, vectors, and references were strings:
glue="0.1275 0.1494 ...",goal="@world/goal". They are tuples and references now, so the parser checks them and every number has its own span. spawn { "@core/x": "pose" }isspawn(robot = @core/x, pose = (...)), andphysics { gravity="..." }isphysics(gravity = (...)), the same shape as every other setting.ComputePathToPose,FollowPath,path="{path}",ClearLocalCostmap,ClearGlobalCostmap,controller=,planner=,PipelineSequence, andTransformAvailableare gone from the task language: they described the navigation stack, not the task.tick_rate_hzonTaskis gone: the previous compiler accepted it and never used it.#comments are gone;/* */comments are new.
What the grammar leaves to later phases
- Whether a name exists and what it is.
- Which parameters a type takes, their types, units, and tuple lengths.
- Whether a reference resolves, and whether its namespace fits the parameter.
- Which node types take children, and whether a task or a robot is complete.
- Whether the target supports an action.