Schemas and Diagnostics Reading Schemas Lesson 11

Value kinds: shapes, vectors, units, bounds

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, 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.

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.

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.

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)

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?