Schemas and Diagnostics Reading Schemas Lesson 13

Cross-references

Mental Model

A cross-reference is a symbol whose allowed values are discovered from the document being validated.

Compare it with chapter 12:

  • A member set like fill-rule gets its legal values from the plugin: evenodd | nonzero.
  • A cross-reference like phrase-name gets its legal values from the document: every (phrase :name ...) form currently in scope.

So a cross-reference has two sides:

(phrase :name p0)       ; declaration: introduces the name p0
(track :sequence [p0])  ; reference: uses the name p0

The validator does two passes conceptually:

  1. Build a registry. Walk the document and collect every target form’s name.
  2. Check references. Every symbol in a cross-reference slot must be present in that registry.

Order in the file does not matter. A reference may appear before the form it names, because the registry is built before references are checked:

(track :sequence [p0])
(phrase :name p0)

The surface value is still just a symbol. You do not write $p0, ref(p0), "p0", or :p0 unless the plugin’s docs explicitly say a different value kind is expected.

Worked Example

You will not see the plugin DSL while authoring. You will see an author-facing summary like this:

(phrase ...)
  :name   symbol required       ; declares a phrase name
  :notes  vector optional

(track ...)
  :sequence vector<phrase-name> required

phrase-name: symbol, cross-reference to (phrase :name ...)

Read it line by line:

  • (phrase ...) :name symbol tells you how names are declared.
  • (track ...) :sequence vector<phrase-name> tells you where names are referenced.
  • phrase-name: ... cross-reference to (phrase :name ...) connects the reference kind to the declaration form.

This source validates:

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

(track :sequence [p0 p1])

Read it as a registry:

declared phrase names: p0, p1
track references:      p0, p1

Both references resolve.

This source does not validate:

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

(track :sequence [p0 p99])

Registry:

declared phrase names: p0
track references:      p0, p99

p99 is missing, so the likely diagnostic is not_cross_ref. Repair by making the reference match a declaration:

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

(track :sequence [p0])

Or repair by adding the missing declaration:

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

(track :sequence [p0 p99])

Names Are Symbols

Most cross-reference declarations use a symbol-valued name key:

(phrase :name p0)

These are different values:

(phrase :name "p0")  ; string
(phrase :name :p0)   ; keyword, and also a keyword-pairing problem

If the plugin says :name symbol, write a bare symbol. A quoted string does not declare the same name. A keyword does not become a normal value after :name; chapter 5’s pairing rule still applies.

The same rule applies at the reference site:

(track :sequence [p0])    ; symbol reference
(track :sequence ["p0"])  ; string, wrong shape
(track :sequence [:p0])   ; keyword element, wrong shape

Duplicate Declarations

A name should identify one target in its scope. If two declarations use the same name, the validator cannot choose which one the reference means.

Broken:

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

(track :sequence [p0])

Registry attempt:

p0 -> first phrase
p0 -> second phrase  ; duplicate

Likely diagnostic: duplicate_cross_ref_target. Repair by renaming one declaration:

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

(track :sequence [p0])

Then choose which one the track should reference:

(track :sequence [p0 p1])

Duplicate checking happens inside the active scope. The next section explains what “scope” means for references.

Scope

When a plugin declares a cross-reference, it also defines where the validator should look for names. The authoring question is:

Which declarations are visible from this reference?

You will usually encounter two practical cases.

Tree Scope

Tree scope is the default authoring model: references resolve against names declared in the same parsed tree, usually one source document or file.

(phrase :name p0)
(track :sequence [p0])

This is the easiest case. If a reference fails, first look in the same document for a declaration with the exact same symbol spelling.

When a host validates several roots as a forest, tooling may build one index for all of them, but ordinary cross-reference checks are still scoped by the rules the plugin and host document. Do not assume a name in another file is visible just because both files are open. For split-file authoring, check the host’s plugin docs.

Lexical Scope

A plugin can make a cross-reference local to the nearest enclosing form. The docs might say:

phrase-name: cross-reference to (phrase :name ...), scope piece

Read that as: a (piece ...) form opens a local registry. References inside a piece can only see phrase names declared inside that same piece.

This validates:

(piece
  (phrase :name p0)
  (phrase :name p1)
  (track :sequence [p0 p1]))

(piece
  (phrase :name p0)
  (track :sequence [p0]))

The two p0 declarations do not collide because they live in different piece scopes.

This does not validate:

(phrase :name p0)
(track :sequence [p0])

If phrase-name is scoped to piece, the reference appears outside any enclosing (piece ...). Likely diagnostic: cross_ref_outside_scope. Repair by moving the declaration and reference into the same scope:

(piece
  (phrase :name p0)
  (track :sequence [p0]))

Another common scoped mistake is referencing a name from a sibling scope:

(piece
  (phrase :name p0))

(piece
  (track :sequence [p0]))

The second piece has no local p0. Likely diagnostic: not_cross_ref. Repair by declaring p0 in the same piece or moving the track into the piece where p0 is declared.

Acyclic References

Some cross-references describe parent chains or dependency chains. In those cases, the plugin may say the reference must be acyclic.

Example contract:

(phrase ...)
  :name   symbol required
  :parent phrase-name optional

phrase-name: cross-reference to (phrase :name ...), acyclic

This chain is fine:

(phrase :name p0 :parent p1)
(phrase :name p1 :parent p2)
(phrase :name p2)

Read the edges:

p0 -> p1
p1 -> p2
p2 -> nothing

There is no loop.

This chain is not fine:

(phrase :name p0 :parent p1)
(phrase :name p1 :parent p0)

Edges:

p0 -> p1
p1 -> p0

The names form a cycle, so the likely diagnostic is cyclic_cross_ref. Repair by breaking the loop:

(phrase :name p0 :parent p1)
(phrase :name p1)

Most cross-references are not acyclic. This rule only matters when the plugin docs explicitly say the kind or key participates in acyclic checking.

Diagnostic Cheat Sheet

DiagnosticWhat it usually meansRepair
not_cross_refThe symbol is not in the visible registry.Fix the spelling, add the declaration, or move the reference into the right scope.
duplicate_cross_ref_targetTwo declarations use the same name in one scope.Rename or remove one declaration.
cross_ref_outside_scopeA scoped reference appears outside its required enclosing form.Put the declaration and reference inside that scope form.
cyclic_cross_refAn acyclic reference chain loops back on itself.Remove or change one edge in the cycle.
wrong_underlyingThe declaration or reference is not the expected value shape, often string vs symbol.Match the plugin’s declared type.

You may also see schema setup diagnostics such as unknown_cross_ref_target, ambiguous_cross_ref_target, cross_ref_name_key_unknown, unknown_cross_ref_scope, or ambiguous_cross_ref_scope. Those usually mean the plugin or manifest is misconfigured, not that an ordinary document reference is misspelled.

Repair Workflow

When a cross-reference fails, do this mechanically:

  1. Find the reference slot in the diagnostic path.
  2. Read the slot’s value kind in the plugin docs.
  3. Find the target form and name key, such as (phrase :name ...).
  4. List the declarations visible from the reference’s scope.
  5. Check exact symbol spelling and case.
  6. If the name exists twice, rename one declaration.
  7. If the kind is acyclic, draw the arrows and remove the loop.

Exercises

Predict the diagnostic, then repair.

Unknown Reference

Assume only p0 and p1 are declared:

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

(track :sequence [p0 p99])

Likely diagnostic: not_cross_ref. Repair by spelling the reference correctly:

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

(track :sequence [p0 p1])

Or by adding the missing target:

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

(track :sequence [p0 p99])

Forward Reference

(track :sequence [p0])
(phrase :name p0)

This should validate. The reference appears first, but the registry is built from the whole document before references are checked.

String Instead Of Symbol

Assume :name expects symbol:

(phrase :name "p0")
(track :sequence [p0])

Likely diagnostics: the declaration has the wrong underlying shape, and the reference may also fail because no symbol name p0 was registered. Repair the declaration:

(phrase :name p0)
(track :sequence [p0])

Duplicate Target

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

(track :sequence [p0])

Likely diagnostic: duplicate_cross_ref_target. Repair by renaming one declaration:

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

(track :sequence [p0])

Outside Lexical Scope

Assume phrase-name is scoped to piece:

(phrase :name p0)
(track :sequence [p0])

Likely diagnostic: cross_ref_outside_scope. Repair by adding the scope form:

(piece
  (phrase :name p0)
  (track :sequence [p0]))

Sibling Lexical Scope

Assume phrase-name is scoped to piece:

(piece
  (phrase :name p0))

(piece
  (track :sequence [p0]))

Likely diagnostic: not_cross_ref. The second piece has no visible p0. Repair by moving the track or declaring the phrase in the same piece:

(piece
  (phrase :name p0)
  (track :sequence [p0]))

Cycle

Assume the plugin opts the :parent key into acyclic detection:

(phrase :name p0 :parent p1)
(phrase :name p1 :parent p0)

Likely diagnostic: cyclic_cross_ref. Repair by breaking the loop:

(phrase :name p0 :parent p1)
(phrase :name p1)

Mastery Check

  1. What are the two sides of a cross-reference?

  2. Why can a reference appear before the form it names?

  3. Why does a typo on a phrase reference produce not_cross_ref instead of not_member?

  4. Why is "p0" not the same declaration as p0?

  5. What is the first repair to try for cross_ref_outside_scope?