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:
| Code | Usual repair direction |
|---|---|
unknown_form / ambiguous_form / ambiguous_expr | Check the head spelling, loaded plugin set, or qualify with plugin/head. |
unknown_key / duplicate_key / missing_required_key | Check the form’s key table. |
positional_not_allowed | Move the child under a containing form or into a form-valued key. |
wrong_underlying / vector_length_mismatch | Match the declared value shape. |
unit_required / unit_not_allowed | Add an allowed unit suffix. |
not_member / not_head_member | Use one of the plugin’s closed-set values or heads. |
union_no_branch_matched | Read 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_mismatch | Rewrite the expression call shape. |
not_cross_ref | Spell the referenced name correctly, or add the missing declaration. |
duplicate_cross_ref_target | Rename one of the two declarations or remove the duplicate. |
cyclic_cross_ref | Break the cycle in the chain (typically a :parent-style key). |
cross_ref_outside_scope | Move the reference inside the enclosing scope form, or declare the name in the right scope. |
missing_discriminant_key | Add 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_present | Remove all but one alternative from the group the message names. |
required_one_of_missing | Add exactly one of the alternatives the message names. |
Worked Example
Broken:
(circle :center [0 0] :radius 1 :fill :evenodd)
Likely symptoms:
:filldoes not pair with:evenodd.- Both become positional flags.
circledoes 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.
| Diagnostic | Quickfix title | What it does |
|---|---|---|
unknown_form | Replace with “ | Substitute the typo with the closest known head (Levenshtein-bounded). |
unknown_key | Replace with : | Substitute the typo with the closest declared key on this form. |
ambiguous_form | Qualify with | One action per claimant plugin. |
missing_required_key | Insert : with stub | Insert each missing required key with a typed placeholder value. |
expr_kvpair_not_allowed | Drop : (keep value) | Strip the keyword tag, leave the value as a positional argument. |
duplicate_key | Remove duplicate : | Delete the later occurrence and its preceding whitespace. |
not_cross_ref | Replace with | Replace the symbol with the closest in-scope registered name. |
cross_ref_outside_scope | Replace with | Surfaces only when an in-scope alternative exists; otherwise no fix (move the reference instead). |
not_member | Replace 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
-
Which diagnostic category usually means you misspelled a key?
-
Which category points to a closed enum-like value?
-
Why should tooling match diagnostic codes rather than message prose?
-
Why can
positional_not_allowedbe caused by keyword pairing? -
For an
exactly-oneexclusive group, which two codes cover "too many" vs. "too few"?