# Diagnostics guidelines How MLF words, structures, and renders errors and warnings. Adapted from rustc's diagnostics philosophy ([rustc dev guide: Errors and lints](https://rustc-dev-guide.rust-lang.org/diagnostics.html)) for our stack: `mlf-lang` produces structured errors, `mlf-diagnostics` renders them through miette for the CLI, and `mlf-wasm`/`mlf-lsp` render them for the playground and editors. ## Principles 1. **Plain English, matter-of-fact.** If a normal programmer can't understand the message, it's too complex. No blame, no jargon from our internals ("workspace resolution", "module symbol table"), no the-user-did-something-"illegal" — prefer "invalid" or something more specific. 2. **Diagnose the mistake, not the symptom.** The resolver knows more at failure time than any renderer does later. Compute targeted help where the context lives (e.g. `Workspace::suggest_for_undefined`) and carry it on the error value, rather than reverse-engineering intent from a message string downstream. 3. **State facts, don't ask questions.** rustc moved away from "did you mean `Foo`?" to "there is a struct with a similar name: `Foo`". Same here: ``a def with a similar name is in scope: `external` ``. 4. **Smallest useful span.** Point at the reference, not the whole field; the primary label should still make sense if it were the only thing displayed. 5. **One error per root cause.** Don't cascade: an unresolved import shouldn't also error every reference that used its binding, when the import error alone explains them. 6. **Every message names the thing.** Reference identifiers, paths, and syntax in backticks: `` `record` ``, `` `use x as y;` ``. ## Anatomy (miette mapping) | rustc concept | ours | |---|---| | level | miette severity | | error code (`E0308`) | `#[diagnostic(code(mlf::…))]` — stable, greppable | | message | the error's `Display`/`message()` — standalone summary | | span labels | `LabeledSpan`s from `mlf-diagnostics` | | help | `get_error_help` / the error's `suggestion` field | | note | additional `LabeledSpan`s or trailing context | **help** is for a concrete change the user can make. **note** is for context and facts. If you can phrase an actionable edit, it's help. ## Wording style - lowercase, no trailing period, unless multiple sentences are needed - succinct: users see the same message many times - name the language "MLF" and the tool by its command (`mlf check`), not "the compiler" - suggestions must be conservative: only offer an edit you'd be comfortable auto-applying or that is obviously checkable by the user ## Current targeted suggestions (wave 1) - `UndefinedReference.suggestion` carries pre-phrased help: - a non-final segment of an import path matches the name → `` `app.bsky.embed.external` is imported as `external` — reference it as `external`, or write `use … as embed;` `` (the exact confusion observed twice in the wild). - an in-scope name within edit distance `max(1, len/3)` → `` a def with a similar name is in scope: `external` ``. - a typo'd item in `use ns.{…}` → nearest def in the target module. ## Wave 2 (done) - **Single source of wording**: `ValidationError::message()` / `::help()` and `ParseError::message()` / `::span()` / `::help()` live in `mlf-lang`; `mlf-diagnostics` (CLI) and `mlf-wasm` (playground) delegate to them. No miette needed on wasm32 — the shared part is plain strings. - **Keyword-as-name hints**: `ParseError::Syntax` carries `help`, attached where the parser still knows the offending token — a keyword in identifier or type position points at the backtick escape. - **`use`-target typos**: unresolvable use targets suggest the nearest known lexicon namespace. ## Wave 3 (done) - **LSP unified**: editor diagnostics use `message()` / `primary_span()` and append `help()` to the diagnostic text, so all three surfaces (CLI, playground, editor) say the same thing. - **Structured constraint errors**: `InvalidConstraint` carries a `ConstraintProblem` class and `ConstraintTooPermissive` carries the loosened constraint key — `help()` matches on structure, never on message substrings. ## Wave 4 (done) - **One report per error** in `mlf check` — each error keeps its own code, caret, and help line instead of hiding behind a "Found N validation error(s)" batch headline. - **Wording pass**: every message and span label follows this doc — lowercase, matter-of-fact, backticked identifiers. Parse errors name tokens honestly via `Token::describe()` ("found keyword `record`", "found `{`", "found end of file"). - **Playground locations + squiggles**: the wasm surface returns structured diagnostics (message, help, span, line/col); errors display with `line:col` prefixes and the editor draws wavy underlines under error spans (a third scroll-synced layer). - **Vague fallback help removed** ("check for missing commas…") — help is only ever site-attached guidance. - **`mlf explain `**: long-form explanations for every `mlf::…` code, rustc `--explain` style. - **LSP**: diagnostics carry the `mlf::…` code and surface help as `relatedInformation` (expandable note with a jump link) instead of a run-on message. ## Wave 5 (done) - **Machine-applicable fixes**: `SuggestedFix { label, span, replacement }` attached at the error site — typo replacements, the `use … as …;` rebinding, keyword backtick escapes. - **LSP quick-fix code actions**: fixes are serialized onto `Diagnostic.data` at publish time and served from `codeAction` without recomputation. The pre-existing organize-imports actions were silently dead (they matched old message prefixes — the exact anti-pattern this doc bans); they now trigger on the stable `mlf::…` code, and the missing-import action reads the name from the diagnostic's range instead of the message. - **Warnings channel**: `resolve()` keeps non-blocking findings (unused imports) on `Workspace::warnings()` instead of dropping them. The CLI renders them as ⚠ reports without failing the check; the LSP publishes them as hints with the removal quick-fix. ## Wave 6 (done) - **Playground "apply fix" buttons**: the wasm diagnostics carry the same `SuggestedFix` data the LSP uses; the playground renders each as a button that applies the edit to the editor (guarded against stale offsets — clicks are ignored if the source changed since the check). - **Typecheck no longer gated behind name resolution**: reference errors, constraint errors, and cycles all surface in one resolve. Safe because the later phases skip unresolved references — findings stay accurate, and users get every actionable error at once. All planned waves are complete. New diagnostics work should follow the principles above; the UI test suite (`tests/diagnostics/`) is the regression net — add a fixture for every new message, hint, or fix.