Schemas and Diagnostics Repair and Style Lesson 14

Diagnostics-driven repair

Mental Model

Diagnostics are not just error messages. They tell you which layer of authoring went wrong:

  • Unknown head or key: vocabulary mismatch.
  • Duplicate or missing key: form contract mismatch.
  • Wrong underlying kind: value shape mismatch.
  • Member, head, or unit error: value kind refinement mismatch.
  • Union of alternatives, no match: the value didn’t fit any alternative the plugin declared for this slot.
  • Expression arity, typed-argument, or kvpair error: expression contract mismatch.
  • Positional error: child placement or keyword-pairing mismatch.
  • Cross-reference resolution failure: name not declared, duplicated, cyclic, or outside its lexical scope.
  • Exclusive-group cardinality error: the form declares an exactly-one-of (or at-most-one-of) rule and the source presents too many or too few alternatives.

Work from the diagnostic code first, then from the prose message.

Stable codes you are likely to see while authoring:

CodeUsual repair direction
unknown_form / ambiguous_form / ambiguous_exprCheck the head spelling, loaded plugin set, or qualify with plugin/head.
unknown_key / duplicate_key / missing_required_keyCheck the form’s key table.
positional_not_allowedMove the child under a containing form or into a form-valued key.
wrong_underlying / vector_length_mismatchMatch the declared value shape.
unit_required / unit_not_allowedAdd an allowed unit suffix.
not_member / not_head_memberUse one of the plugin’s closed-set values or heads.
union_no_branch_matchedRead the listed alternatives - the message names every shape the slot accepts - and rewrite the value to fit one of them.
expr_kvpair_not_allowed / arity_mismatch / expr_type_mismatchRewrite the expression call shape.
not_cross_refSpell the referenced name correctly, or add the missing declaration.
duplicate_cross_ref_targetRename one of the two declarations or remove the duplicate.
cyclic_cross_refBreak the cycle in the chain (typically a :parent-style key).
cross_ref_outside_scopeMove the reference inside the enclosing scope form, or declare the name in the right scope.
missing_discriminant_keyAdd the discriminant key (e.g. :kind) to the form.
unknown_key (with discriminant-first hint)Reorder so the discriminant key precedes any variant-only key.
mutually_exclusive_keys_presentRemove all but one alternative from the group the message names.
required_one_of_missingAdd exactly one of the alternatives the message names.

Worked Example

Broken:

(circle :center [0 0] :radius 1 :fill :evenodd)

Likely symptoms:

  • :fill does not pair with :evenodd.
  • Both become positional flags.
  • circle does not accept positional children.

The real mistake is not “fill rule unknown”; it is keyword pairing. Repair:

(circle :center [0 0] :radius 1 :fill evenodd)

Broken:

(badge :label "x" :shape (group :name "oops"))

Likely diagnostic: not_head_member. The :shape slot accepts only specific form heads. Repair with an allowed form:

(badge :label "x" :shape (circle :center [0 0] :radius 1))

Exercises

Predict the diagnostic category and repair each example.

Unknown form:

(circl :center [0 0] :radius 1)

Repair:

(circle :center [0 0] :radius 1)

Unknown key:

(canvas :width 320 :height 240)

Repair:

(canvas :w 320 :h 240)

Duplicate key:

(scene :title "a" :title "b")

Repair by choosing one value:

(scene :title "b")

Missing required key. This one is a schema-reading drill: assume the plugin docs say (circle ...) requires both :center and :radius.

(circle :center [0 0])

Repair:

(circle :center [0 0] :radius 1)

Wrong underlying kind:

(circle :center "middle" :radius 1)

Repair:

(circle :center [0 0] :radius 1)

Arity mismatch:

(shape :sdf :radius (lerp 0 10))

Repair:

(shape :sdf :radius (lerp 0 10 0.5))

Typed expression mismatch:

(vec3 1 "two" 3)

vec3 takes three numbers:

(vec3 1 2 3)

If you meant a scalar radius, use a scalar expression instead:

(shape :sdf :radius (* 2 16))

Member set mismatch:

(circle :center [0 0] :radius 1 :fill diagonal)

Repair:

(circle :center [0 0] :radius 1 :fill nonzero)

Unit mismatch. Assume duration allows s | ms | b:

(delay :wait 4px)

Repair with an allowed suffix:

(delay :wait 4b)

Head set mismatch:

(badge :label "x" :shape (group :name "oops"))

Repair:

(badge :label "x" :shape (rect :origin [0 0] :size [10 10]))

Union no-branch matched. Assume :notes is documented as vector<note-or-event> where note-or-event = note-or-rest | event and event is a form with head n or rest:

(phrase :notes [E4 42])

Likely diagnostic: union_no_branch_matched listing the alternatives (note-or-rest, event). The number 42 is neither a pitch symbol nor an event form. Repair with a value matching either alternative:

(phrase :notes [E4 (n G4 0.5b)])

Union with form alternative - the wildcard pitfall. Assume the plugin documents :value as number | vec4 | form:

(set :value (foo 1 2))

Likely diagnostic: unknown_form (not union_no_branch_matched). The form alternative still resolves the form’s head against the schema; “any form” is not the same as “any parens.” Repair by using a form whose head the schema knows:

(set :value (+ 1 2))

Ambiguous form. Assume two loaded plugins both declare circle:

(circle :center [0 0] :radius 1)

Repair by qualifying the domain head:

(shapes/circle :center [0 0] :radius 1)

Discriminant absent. Assume (track ...) is documented with discriminant: kind and variants kick | groove | animation:

(track :name k1 :from 0)

Likely diagnostic: missing_discriminant_key. Repair:

(track :kind kick :name k1 :step 4 :from 0)

Discriminant out of order. The variant key :step is written before :kind:

(track :name k1 :step 4 :kind kick)

Likely diagnostic: unknown_key on :step with a hint that the discriminant must be set first. Repair by placing :kind before any variant-only key:

(track :kind kick :name k1 :step 4)

Cross-variant key. :mesh is an animation-variant key, but :kind = kick:

(track :kind kick :name k1 :mesh logo)

Likely diagnostic: unknown_key (no hint - discriminant is set, the key just isn’t valid under this variant). Repair by changing the discriminant or removing the wrong-variant key:

(track :kind animation :name k1 :mesh logo)

Exclusive-group violation. Assume (phrase ...) is documented with an exactly-one group over :notes | :events:

(phrase :name p0 :notes [E4 G4] :events [(n A4 0.5b)])

Likely diagnostic: mutually_exclusive_keys_present listing :notes | :events. Repair by keeping one alternative:

(phrase :name p0 :notes [E4 G4])

Required-one-of missing. Same (phrase ...) summary:

(phrase :name p1)

Likely diagnostic: required_one_of_missing. Repair by adding one of the alternatives:

(phrase :name p1 :events [(n A4 0.5b)])

Cross-reference miss. Assume the plugin’s :sequence slot expects vector<phrase-name> and only p0 is declared:

(phrase :name p0 :notes [E4 G4 A4 G4])

(track :sequence [p0 p1])

Likely diagnostic: not_cross_ref at [track sequence]. Repair by adding the missing phrase or fixing the spelling:

(phrase :name p0 :notes [E4 G4 A4 G4])
(phrase :name p1 :notes [B4 A4 G4 E4])

(track :sequence [p0 p1])

Chapter 13 covers cross-reference shapes in detail.

LSP Quickfixes

SJON’s language server offers single-step quickfixes for the diagnostics it can repair without guessing. The action title tells you what it will do; review before applying.

DiagnosticQuickfix titleWhat it does
unknown_formReplace with Substitute the typo with the closest known head (Levenshtein-bounded).
unknown_keyReplace with :Substitute the typo with the closest declared key on this form.
ambiguous_formQualify with /“One action per claimant plugin.
missing_required_keyInsert : with stubInsert each missing required key with a typed placeholder value.
expr_kvpair_not_allowedDrop : (keep value)Strip the keyword tag, leave the value as a positional argument.
duplicate_keyRemove duplicate :Delete the later occurrence and its preceding whitespace.
not_cross_refReplace with Replace the symbol with the closest in-scope registered name.
cross_ref_outside_scopeReplace with Surfaces only when an in-scope alternative exists; otherwise no fix (move the reference instead).
not_memberReplace with Replace the value with the closest non-deprecated member of the slot’s enum.

For typo-style fixes, only the single closest candidate within Levenshtein distance 3 is offered; farther typos return no action so the suggestion can’t mislead.

Mastery Check

  1. Which diagnostic category usually means you misspelled a key?

  2. Which category points to a closed enum-like value?

  3. Why should tooling match diagnostic codes rather than message prose?

  4. Why can positional_not_allowed be caused by keyword pairing?

  5. For an exactly-one exclusive group, which two codes cover "too many" vs. "too few"?