Skip to content

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 ​

FieldMeaning
codeStable 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.
messageOne sentence naming the cause.
levelPresentation only: error, warning, note, help.
phaseThe compiler phase that produced it (below).
failure_domainWho owns the failure: application (the program or a library), invocation (command-line arguments), environment (files the compiler cannot read or write).
base_disposition, effective_dispositionWhether 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.
primaryThe span that caused the diagnostic, with a short label.
relatedOther spans that explain it, each with a label, for example the first declaration of a duplicate name.
editsMachine-applicable replacements, if the fix is certain.
helpWhat the reader should change. One short line per entry.
notesThe 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 ​

CodePhaseCause
invocation.unknown_source_kind, invocation.duplicate_source_kind, invocation.source_missingsource_loada 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_inputsource_loada --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_unreadablesource_loada source file the compiler cannot read
syntax.unexpected_tokenparsea token no rule allows here; the message says what was expected, in plain words
syntax.unexpected_characterparsea character that starts no token, for example #
syntax.unexpected_eofparsethe source ends inside a declaration
syntax.invalidparseany other parser failure
syntax.unknown_reference_namespaceparsea reference whose namespace is not core, world, or task; the declaration is still built
syntax.number_out_of_rangeparsea number too large for a 64-bit float, such as 1e999
syntax.duplicate_parameterparsea parameter given twice in one argument list; the primary is the second occurrence and the first is related
library.*library_loadproblems in a component library; listed in Component library format
name.unknown_component_type, name.unknown_entity_type, name.unknown_node_type, name.unknown_checkname_resolutiona 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_supportedname_resolutionthe 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_objectname_resolutionthe 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_environmentname_resolutionthe 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_referencename_resolutionan @core/, @world/ or @task/ reference that nothing declares
name.duplicatename_resolutiontwo components, entities or goals with one name, or physics/spawn given twice
name.chassis_missing, name.multiple_chassisname_resolutiona robot needs exactly one chassis
type.unknown_parameter, type.missing_parametertype_checkan 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_quaterniontype_checkan argument of the wrong shape or an impossible value
type.wrong_referencetype_checka reference to the wrong namespace or kind, for example Navigate(goal = @core/...)
type.exactly_one_oftype_checkan entity needs one of type/ref; a goal one of pose/target
type.size_needs_typetype_checkan entity states size beside ref; an object has the size its library entry states
type.second_ground_planetype_checka 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_collisiontype_checkan 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_requiredtype_checka 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_invertedtype_checka check without min or max, or with min above max
validation.<rule>validationa 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.unsupportedtarget_bindinga construct the chosen target cannot realize; Target output lists them
target.blind_navigationtarget_bindinga warning: the task navigates, but the robot has no range sensor to feed the costmaps
environment.output_unwritableoutput_writethe output directory cannot be written
environment.output_linkedoutput_writea 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:

text
<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:

text
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:

text
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 ​

json
{
  "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 help and the facts in notes; 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.