A human-friendly DSL for ATProto Lexicons mlf.lol
atproto lexicons cli dns
mlf docs diagnostics.md
7.0 kB · 143 lines
Markdown
at main

Diagnostics guidelines #

How MLF words, structures, and renders errors and warnings. Adapted from rustc's diagnostics philosophy (rustc dev guide: Errors and lints) 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 LabeledSpans from mlf-diagnostics
help get_error_help / the error's suggestion field
note additional LabeledSpans 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 <code>: 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.