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:

  1. Underlying shape - should the value be a number, string, symbol, vector, form, or union?
  2. Refinement - does the kind add a vector length, unit rule, representation tag, closed member list, allowed head list, or list of alternatives?
  3. 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 point means “write a vector with two numeric elements.”
  • :radius length means “write a number.”
  • :fill fill-rule means “write one of the listed symbols.”

Read the second form:

  • :label string means “write quoted text.”
  • :shape shape-form means “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 true and value ≤ :min.
  • number_at_or_above_exclusive_max:exclusive-max true and value ≥ :max.
  • number_not_integer:integer true and 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. u16 is [0, 65535], u32 is [0, 2^32), and i32 is 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

  1. Is a plugin-declared value kind a new SJON syntax feature?

  2. When reading a named kind, what should you check first?

  3. Which diagnostic points to a missing required unit suffix?

  4. Which diagnostic points to a vector with the wrong number of elements?

  5. A slot is typed attribute: vector, length 2-4, element number. Which value is rejected?

  6. A number kind sets unit rejected. What does it accept?

  7. A value typed repr u16 is rejected. What should you check about the number?