Diagnostics
Every phase reports problems through one diagnostic type, defined in src/mobiledsl/support/diagnostics.py. A diagnostic points at the source bytes that caused it, says what to change, and states the facts the decision rests on. The same object renders as a terminal line and as JSON.
The diagnostic
| Field | Meaning |
|---|---|
code | Stable identifier: two or more dotted lower_snake_case segments, for example syntax.unexpected_token, name.duplicate. Codes never change meaning; a changed check gets a new code. |
message | One sentence naming the cause. |
level | Presentation only: error, warning, note, help. |
phase | The compiler phase that produced it (below). |
failure_domain | Who owns the failure: application (the program or a library), invocation (command-line arguments), environment (files the compiler cannot read or write). |
base_disposition, effective_disposition | Whether it blocks compilation: blocking or non_blocking. The base value is the check's own judgement; the effective value may be changed by invocation policy, and then disposition_reason says why. |
primary | The span that caused the diagnostic, with a short label. |
related | Other spans that explain it, each with a label, for example the first declaration of a duplicate name. |
edits | Machine-applicable replacements, if the fix is certain. |
help | What the reader should change. One short line per entry. |
notes | The facts the decision rests on. One short line per entry. |
Level and blocking are independent. An error is normally blocking and a warning normally not, but --warnings-as-errors changes the effective disposition of a warning without changing its level, and the reason is recorded on the diagnostic.
Phases
In execution order: source_load, library_load, target_resolution, parse, name_resolution, type_check, analysis, validation, target_binding, world_realization, emission, output_write. Each phase ends in one status: pass, invalid, environment_failed, not_run, or not_requested.
Codes
| Code | Phase | Cause |
|---|---|---|
invocation.unknown_source_kind, invocation.duplicate_source_kind, invocation.source_missing | source_load | a file that is not .core, .world, .task or .eval, two files of one form, or a required form not given |
invocation.unknown_target, invocation.no_target, invocation.no_output, invocation.output_overlaps_input | source_load | a --target that does not exist, --output without --target or the reverse, or an output directory that is, holds, or sits inside an input |
environment.source_unreadable | source_load | a source file the compiler cannot read |
syntax.unexpected_token | parse | a token no rule allows here; the message says what was expected, in plain words |
syntax.unexpected_character | parse | a character that starts no token, for example # |
syntax.unexpected_eof | parse | the source ends inside a declaration |
syntax.invalid | parse | any other parser failure |
syntax.unknown_reference_namespace | parse | a reference whose namespace is not core, world, or task; the declaration is still built |
syntax.number_out_of_range | parse | a number too large for a 64-bit float, such as 1e999 |
syntax.duplicate_parameter | parse | a parameter given twice in one argument list; the primary is the second occurrence and the first is related |
library.* | library_load | problems in a component library; listed in Component library format |
name.unknown_component_type, name.unknown_entity_type, name.unknown_node_type, name.unknown_check | name_resolution | a type name the vocabulary does not have; help suggests a near miss |
name.unknown_part, name.ambiguous_part, name.part_category_mismatch, name.type_not_supported | name_resolution | the ref of a component names no part, several parts, a part of another category, an object or an environment, or a part without the type's profile |
name.unknown_object, name.ambiguous_object, name.not_an_object | name_resolution | the ref of a world entity names no object or one that could not be loaded, an object several libraries have, or a part of a robot or an environment |
name.unknown_environment, name.ambiguous_environment, name.not_an_environment | name_resolution | the ref of a scenario names no environment or one that could not be loaded, an environment several libraries have, or a part or an object. A world name the program does not declare is not reported beside it: it may be the missing environment's |
name.unknown_reference | name_resolution | an @core/, @world/ or @task/ reference that nothing declares |
name.duplicate | name_resolution | two components, entities or goals with one name, or physics/spawn given twice |
name.chassis_missing, name.multiple_chassis | name_resolution | a robot needs exactly one chassis |
type.unknown_parameter, type.missing_parameter | type_check | an argument the type does not take, or a required one that is absent |
type.wrong_value_type, type.wrong_tuple_length, type.invalid_choice, type.not_positive, type.not_unit_vector, type.not_unit_quaternion | type_check | an argument of the wrong shape or an impossible value |
type.wrong_reference | type_check | a reference to the wrong namespace or kind, for example Navigate(goal = @core/...) |
type.exactly_one_of | type_check | an entity needs one of type/ref; a goal one of pose/target |
type.size_needs_type | type_check | an entity states size beside ref; an object has the size its library entry states |
type.second_ground_plane | type_check | a scenario states ground_plane = true in a world that already has its floor: an environment of any of its scenarios brings one, or another scenario lays it |
type.object_without_collision | type_check | an entity that collides, an Obstacle, a SupportSurface or one with collision = true, is made of an object that declares no collision |
type.children_required, type.children_not_allowed, type.one_child_required | type_check | a composite without a child node or a task without a node, a block under an action, or a decorator without exactly one child |
type.bound_required, type.bounds_inverted | type_check | a check without min or max, or with min above max |
validation.<rule> | validation | a rule that failed (an error, or a warning for an advisory rule) or could not decide (a warning); the rules are listed in Validation rules |
target.unsupported | target_binding | a construct the chosen target cannot realize; Target output lists them |
target.blind_navigation | target_binding | a warning: the task navigates, but the robot has no range sensor to feed the costmaps |
environment.output_unwritable | output_write | the output directory cannot be written |
environment.output_linked | output_write | a directory inside the output is a symbolic link, which the compiler does not write through |
The parser stops at the first syntax error, so a source with several syntax errors reports one per compile. Every later phase runs over the whole input and reports every occurrence, so one compile shows everything it can.
Locations
A span is a half-open range of UTF-8 bytes in one source, identified by the name the compiler was given for that source, usually its path. Spans are exact: a diagnostic about a name covers exactly the bytes of that name.
For people, a byte offset is shown as a one-based line and a one-based column counted in Unicode scalar values. Line terminators may be LF or CRLF. JSON output carries both the byte offset and the line and column.
Terminal form
The terminal form follows rustc and ruff, so a reader who knows those tools needs no new convention:
<level>[<code>]: <message>
--> <source>:<line>:<column>
|
N | <source line>
| ------ <related label>
...
M | <source line>
| ^^^^^^ <primary label>
|
= help: <what to change>
= help: <edit message> at <source>:<line>:<column>: '<replacement>'
= note: <fact>
= note: <related label> at <other source>:<line>:<column>The heading names the level, the code, and the cause. The arrow gives the primary location. The excerpt shows every source line that carries a span: the primary span is underlined with carets, related spans in the same source with dashes, and lines with nothing to show are elided as .... A span that continues onto later lines is underlined to the end of its first line; an empty span, such as the insertion point for a missing declaration, gets one caret. Tabs are shown as four spaces. The footer lists help (from help and from machine edits) and note (from notes, from related spans in other sources, from related spans beyond the first three as and N more, and the reason when the effective disposition differs from the base). A span whose source is not loaded is shown as <source>:bytes <start>..<end> with no excerpt.
Color is applied when the output is a terminal. NO_COLOR disables it, FORCE_COLOR enables it for a pipe, and TERM=dumb disables it; this is the order those variables have in other CLIs. The level and the primary underline use the level's color (error red, warning yellow, note cyan, help green), the message is bold, and the gutter, arrows, and related underlines are blue. Color never changes the text.
Example, a duplicate name:
error[name.duplicate]: component 'wheel_lf' is declared twice
--> demo.core:18:13
|
17 | Mecanum wheel_lf(ref = "mecanum_black", glue = (0.1275, 0.1494, -0.0216, 0, 1, 0))
| -------- first declared here
18 | Mecanum wheel_lf(ref = "mecanum_black", glue = (-0.1275, 0.1494, -0.0216, 0, 1, 0))
| ^^^^^^^^ declared again here
|Example, a syntax error:
error[syntax.unexpected_token]: unexpected 'glue'; expected one of ')', ','
--> demo.core:22:40
|
22 | Lidar top_lidar(ref = "lidar_ld06" glue = (0, 0.0001, 0.0344, 0, 0, -1))
| ^^^^ not allowed here
|JSON form
{
"code": "name.duplicate",
"level": "error",
"message": "component 'wheel_lf' is declared twice",
"phase": "name_resolution",
"failure_domain": "application",
"disposition": {"base": "blocking", "effective": "blocking"},
"primary": {
"span": {
"source": "demo.core",
"start": {"byte_offset": 1163, "line": 18, "column": 13},
"end": {"byte_offset": 1171, "line": 18, "column": 21}
},
"label": "declared again here"
},
"related": [
{
"span": {
"source": "demo.core",
"start": {"byte_offset": 1076, "line": 17, "column": 13},
"end": {"byte_offset": 1084, "line": 17, "column": 21}
},
"label": "first declared here"
}
]
}This is the same diagnostic as the terminal example above.
Keys that would be empty (related, edits, help, notes) are omitted. disposition.reason is present only when the effective disposition differs from the base. An edit has applicability (only machine_applicable exists), a span, a replacement, and an optional message.
Ordering
A stream of diagnostics is sorted before it is shown or serialized, so the output does not depend on traversal order. The key is: phase order, then primary span (source name, start byte, end byte, label), then level, code, message, failure domain, dispositions, then the related spans and edits.
Machine edits
An edit replaces one span with UTF-8 text. Edits within one source must not overlap and must not share a start offset. Applying them never mutates the loaded sources; the result is new bytes per source. Only edits whose correctness is certain are attached; a suggestion the author has to judge is help text.
Rules for writing a diagnostic
- Point at the bytes that caused the problem, in the program or in the part file. A diagnostic with no span is a last resort for environment failures.
- Name the cause in the message. Put what to change in
helpand the facts innotes; do not mix them. - Never resolve an ambiguity for the author. Report every candidate as related and produce no result.
- A missing value is
unknown, never a default; say which value is missing and where it is declared. - The code is part of the interface: tests and repair tools match on it.