Schemas and Diagnostics Reading Schemas Lesson 11
Value kinds: shapes, vectors, units, bounds, representation
Mental Model
The phrase “value kind” appears in two related places:
- The parser sees base value kinds:
number,string,symbol,vector,form, and so on. - A plugin can give a name to a narrower contract:
length,point,fill-rule,shape-form,duration,note-or-event.
The named kind is not new syntax. If a slot is typed point, you do not
write point(160 120) or :point [160 120]. You write the value whose
shape satisfies the kind:
(circle :center [160 120])
Read a named kind in this order:
- Underlying shape - should the value be a number, string, symbol, vector, form, or union?
- Refinement - does the kind add a vector length, unit rule, representation tag, closed member list, allowed head list, or list of alternatives?
- Surface value - what do you actually type in the document?
You do not define value kinds while authoring a document. You read the plugin docs and write values that satisfy them.
Cross-references are also implemented as value-kind refinements, but they deserve their own authoring model. Chapter 13 covers them.
Worked Example
The reference shapes plugin can be summarized like this:
(circle ...)
:center point optional
:radius length optional
:fill fill-rule optional
(badge ...)
:label string optional
:shape shape-form optional
length: number
point: vector, length 2, element number
fill-rule: symbol, members evenodd | nonzero
shape-form: form, heads circle | rect
Now write the source from the contract:
(circle :center [160 120] :radius 32 :fill evenodd)
(badge :label "dot"
:shape (circle :center [0 0] :radius 1))
Read the first form slowly:
:center pointmeans “write a vector with two numeric elements.”:radius lengthmeans “write a number.”:fill fill-rulemeans “write one of the listed symbols.”
Read the second form:
:label stringmeans “write quoted text.”:shape shape-formmeans “write a nested form, but only with an allowed head.”
The important habit: start from the slot line, then jump to the named kind line. The slot tells you which kind applies; the kind tells you which value shape is accepted.
Underlying Shape First
Before worrying about refinements, make the broad shape match.
(circle :center "160,120" :radius 32)
This fails before the validator even cares about length 2. point is
vector-underlying, so a string is the wrong underlying shape. Repair by
using brackets:
(circle :center [160 120] :radius 32)
Likewise:
(circle :center [160 120] :radius "32")
:radius is length, and length is number-underlying. A string
produces wrong_underlying. Repair with a number:
(circle :center [160 120] :radius 32)
Vector Shapes
A vector refinement usually answers two questions:
- How many elements?
- What kind should each element have?
For point:
point: vector, length 2, element number
That accepts:
[0 0]
[160 120]
[1.5 -2]
It rejects a vector with too few or too many elements:
(circle :center [160] :radius 32)
Likely diagnostic: vector_length_mismatch. Repair by writing exactly
two elements:
(circle :center [160 120] :radius 32)
It also rejects a vector whose element has the wrong kind:
(circle :center [160 "top"] :radius 32)
The second element is a string, but point needs numbers. Repair the
element:
(circle :center [160 120] :radius 32)
Nested vectors use the same idea. If a plugin documents
points: vector, element point, then the outer value is a vector and
each element must satisfy point:
(shape :points [[0 0] [1 0] [1 1]])
This is not the same shape:
(shape :points [0 0 1 0 1 1])
That is one flat vector of numbers, not a vector of points.
Variable-Length Vectors
A vector kind does not have to fix an exact length. Instead of a single
length, a plugin can set a minimum, a maximum, or both; the value is
then any vector whose element count lands in that window. The examples
here come from a GPU-oriented plugin, where a vertex attribute is two to
four float components.
attribute: vector, length 2-4, element number
attribute accepts a vec2, vec3, or vec4:
(vertex :position [0.0 1.0])
(vertex :position [0.0 1.0 0.5])
(vertex :position [0.0 1.0 0.5 1.0])
Too few elements:
(vertex :position [0.0])
Likely diagnostic: vector_too_short. Too many:
(vertex :position [0.0 1.0 0.5 1.0 2.0])
Likely diagnostic: vector_too_long. Repair by writing a vector whose
length lands in the documented range:
(vertex :position [0.0 1.0 0.5])
This is a different contract from a fixed length. A fixed-length kind
like point accepts exactly two elements and fires
vector_length_mismatch for anything else; a variable-length kind
accepts a range and fires vector_too_short or vector_too_long at the
edges. Read the contract to know which rule applies.
Unit Shapes
A unit refinement applies to a number-underlying kind. The plugin may allow unitless numbers, require a unit, or allow only specific suffixes.
Example contract:
duration: number, unit required, allowed s | ms | b
Accepted values:
0.5s
250ms
4b
Rejected because the unit is missing:
(delay :wait 4)
Likely diagnostic: unit_required. Repair with an allowed suffix:
(delay :wait 4b)
Rejected because the suffix is not allowed:
(delay :wait 90deg)
Likely diagnostic: unit_not_allowed. Repair by using one of the
suffixes in the kind contract:
(delay :wait 250ms)
Remember: SJON preserves unit suffixes but does not interpret them.
The plugin decides whether b, ms, or s means anything useful.
Rejecting Units
The opposite of requiring a unit is rejecting every unit. A kind that
sets unit rejected accepts a bare number and nothing else.
raw-uniform: number, unit rejected
Accepted:
(draw :lod-bias 0.5)
Rejected, because the value carries a suffix:
(draw :lod-bias 0.5f)
Likely diagnostic: unit_forbidden. Repair by dropping the suffix:
(draw :lod-bias 0.5)
This rule earns its place. The lexer reads the trailing f in 0.5f
as a unit suffix, so without a reject rule a value you meant as a plain
float lands as a number-with-unit, and a consumer that ignores units
could read it as 0. A reject kind turns that into a diagnostic at the
value site instead of a wrong number downstream.
Numeric Bounds
A numeric bound refinement applies to a number-underlying kind and constrains the value’s magnitude or integrality, orthogonal to any unit shape. The plugin may pin a minimum, maximum, either-exclusive, or require integer values.
Example contracts:
opacity: number, range [0, 1]
iteration-count: number, min 1, integer
duration-ms: number, unit required ms, range [0ms, 10000ms]
Accepted values for opacity:
0
0.5
1
Rejected because below :min:
(layer :opacity -0.1)
Likely diagnostic: number_below_min. Other diagnostics in this
family:
number_above_max— value greater than:max.number_at_or_below_exclusive_min—:exclusive-min trueand value ≤:min.number_at_or_above_exclusive_max—:exclusive-max trueand value ≥:max.number_not_integer—:integer trueand value is fractional or non-finite.numeric_bound_unit_mismatch— bound carries a unit but the value either has none or carries a different unit.
Comparison preserves exact precision when both the bound and the
value came from integer literals (9007199254740993 vs a :max
of 9007199254740992 correctly fires number_above_max, even
though both round to the same f64). For everyday plugins this
just works; the corner only matters when the bound itself
approaches 2^53.
Representation
A representation tag pins the machine type a downstream tool will encode
a number as - u16, u32, i32, f32, or f16. The value you write
is still an ordinary SJON number; the tag tells the validator to check
that the number actually fits that type.
channel: number, repr u16
scalar: number, repr f32
vertex-index: number, repr u32
Two things are checked, both at validate time:
- Range - the number must fall inside the type’s span.
u16is[0, 65535],u32is[0, 2^32), andi32is the signed 32-bit range. - Integrality - an integer type (
u16,u32,i32) rejects a fractional value. A float type (f32,f16) carries no integrality rule; any finite number in range is accepted.
Accepted:
(vertex :tint 65535)
(draw :line-width 1.5)
(draw :base-vertex 32768)
65535 fits u16, 1.5 is a fine f32, and 32768 fits u32.
Out of range:
(vertex :tint 70000)
Likely diagnostic: repr_out_of_range - 70000 is above the u16
ceiling of 65535. The same code covers a non-integral value under an
integer type:
(draw :base-vertex 1.5)
Here the message names the integrality failure rather than the range. Repair by writing a number that fits - in range and, for an integer type, whole:
(vertex :tint 65535)
One thing a repr tag does not do: it does not ask you to round for
precision. An f32 value that needs more than 32 bits of mantissa is
still accepted - the precision narrowing is the downstream encoder’s
step, not a validation error. The tag guards range and integrality,
nothing more.
Exercises
For each exercise, read the contract first, then repair the source.
Vector Shape
Contract:
point: vector, length 2, element number
(circle ...)
:center point optional
(circle :center [160] :radius 32)
Repair:
(circle :center [160 120] :radius 32)
Vector Element Kind
(circle :center [160 "top"] :radius 32)
Repair:
(circle :center [160 120] :radius 32)
Unit Shape
Contract:
duration: number, unit required, allowed s | ms | b
(delay ...)
:wait duration required
(delay :wait 4)
Repair:
(delay :wait 4b)
Variable-Length Vector
Contract:
attribute: vector, length 2-4, element number
(vertex ...)
:position attribute required
(vertex :position [0.0])
Repair:
(vertex :position [0.0 1.0])
Unit Rejection
Contract:
raw-uniform: number, unit rejected
(draw ...)
:lod-bias raw-uniform optional
(draw :lod-bias 0.5f)
Repair:
(draw :lod-bias 0.5)
Representation
Contract:
channel: number, repr u16
(vertex ...)
:tint channel optional
(vertex :tint 70000)
Repair:
(vertex :tint 65535)
Mastery Check
-
Is a plugin-declared value kind a new SJON syntax feature?
-
When reading a named kind, what should you check first?
-
Which diagnostic points to a missing required unit suffix?
-
Which diagnostic points to a vector with the wrong number of elements?
-
A slot is typed
attribute: vector, length 2-4, element number. Which value is rejected? -
A number kind sets
unit rejected. What does it accept? -
A value typed
repr u16is rejected. What should you check about the number?