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-rulegets its legal values from the plugin:evenodd | nonzero. - A cross-reference like
phrase-namegets 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:
- Build a registry. Walk the document and collect every target form’s name.
- 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 symboltells 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
| Diagnostic | What it usually means | Repair |
|---|---|---|
not_cross_ref | The symbol is not in the visible registry. | Fix the spelling, add the declaration, or move the reference into the right scope. |
duplicate_cross_ref_target | Two declarations use the same name in one scope. | Rename or remove one declaration. |
cross_ref_outside_scope | A scoped reference appears outside its required enclosing form. | Put the declaration and reference inside that scope form. |
cyclic_cross_ref | An acyclic reference chain loops back on itself. | Remove or change one edge in the cycle. |
wrong_underlying | The 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:
- Find the reference slot in the diagnostic path.
- Read the slot’s value kind in the plugin docs.
- Find the target form and name key, such as
(phrase :name ...). - List the declarations visible from the reference’s scope.
- Check exact symbol spelling and case.
- If the name exists twice, rename one declaration.
- 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
-
What are the two sides of a cross-reference?
-
Why can a reference appear before the form it names?
-
Why does a typo on a phrase reference produce
not_cross_refinstead ofnot_member? -
Why is
"p0"not the same declaration asp0? -
What is the first repair to try for
cross_ref_outside_scope?