Satin Studio Manual

Dither Language Specification

This page defines the source language accepted by Dither. It is the formal companion to Getting started with Dither: the tutorial teaches by example, while this specification settles questions about spelling, structure, evaluation order, and valid forms.

The grammar describes the language itself. Satin Studio commands and installed domain modules add callable names, but they do not change the reader grammar.

1. Conformance language

The words must, must not, should, and may describe requirements.

A conforming reader must accept every source unit described by the reader grammar and reject mismatched delimiters, unterminated strings, odd map entry counts, invalid numeric literals, and invalid dither:// URI literals.

A conforming evaluator must evaluate arguments from left to right, preserve collection order, attach source locations to diagnostics when available, and stop synchronous evaluation at its resource bound.

2. Grammar notation

The grammar uses an extended BNF notation:

<name>       ::= one production
               | another production

<x>...       ::= zero or more repetitions of <x>
<x>+         ::= one or more repetitions of <x>
<x>?         ::= zero or one <x>

"text"       ::= exact source text
U+0020       ::= a Unicode code point
A - B        ::= characters in A except those in B

Spacing shown inside a production is explanatory. Actual forms are separated by one or more separators as defined below.

3. Complete reader grammar

<source-unit> ::= <separator>* <form-sequence>? <separator>*
<form-sequence> ::= <form> (<separator>+ <form>)*

<form> ::= <nil>
         | <boolean>
         | <integer>
         | <float>
         | <string>
         | <keyword>
         | <uri>
         | <symbol>
         | <list>
         | <vector>
         | <map>
         | <quoted-form>

<list> ::= "(" <separator>* <form-sequence>? <separator>* ")"
<vector> ::= "[" <separator>* <form-sequence>? <separator>* "]"
<map> ::= "{" <separator>* <map-entries>? <separator>* "}"
<map-entries> ::= <form> <separator>+ <form>
                  (<separator>+ <form> <separator>+ <form>)*
<quoted-form> ::= "'" <separator>* <form>

<separator> ::= <whitespace>
              | ","
              | <line-comment>
<whitespace> ::= U+0009 | U+000A | U+000D | U+0020
               | <other-unicode-whitespace>
<line-comment> ::= ";" <comment-character>* (<line-ending> | <end-of-source>)
<comment-character> ::= <unicode-scalar> - U+000A
<line-ending> ::= U+000A

<nil> ::= "nil"
<boolean> ::= "true" | "false" | "t"

<integer> ::= <sign>? <decimal-digit>+
<float> ::= <sign>? <decimal-mantissa> <exponent>?
<decimal-mantissa> ::= <decimal-digit>+ "." <decimal-digit>*
                     | "." <decimal-digit>+
<exponent> ::= ("e" | "E") <sign>? <decimal-digit>+
<sign> ::= "+" | "-"
<decimal-digit> ::= "0" | "1" | "2" | "3" | "4"
                  | "5" | "6" | "7" | "8" | "9"

<string> ::= "\"" <string-character>* "\""
<string-character> ::= <unicode-scalar> - ("\"" | "\\")
                     | <escape>
<escape> ::= "\\n" | "\\r" | "\\t" | "\\\"" | "\\\\"
           | "\\" <unicode-scalar>

<keyword> ::= ":" <atom-tail>*
<uri> ::= "dither://" <atom-tail>+
<symbol> ::= <atom-character> <atom-tail>*

<atom-tail> ::= <atom-character>
<atom-character> ::= <unicode-scalar>
                   - (<unicode-whitespace> | "," | "(" | ")"
                      | "[" | "]" | "{" | "}" | "'" | "\"" | ";")

Token classification follows this order: reserved literal, keyword, Dither URI, integer, float, then symbol. For example, true is a Boolean rather than a symbol, and :true is a keyword.

A floating-point token must contain a decimal point. 1.0e3 is a float; 1e3 is a symbol. Numeric values must be finite. Integers use the signed 64-bit range.

A comma is a separator, not collection punctuation. [1, 2, 3] and [1 2 3] read identically. Canonical formatting omits commas.

Quote shorthand is reader expansion:

"'" <form> ::= "(" "quote" <separator> <form> ")"

4. Values

Dither source produces these values:

ValueExamplesEvaluation
NilnilEvaluates to itself and is falsey.
Booleantrue, false, tEvaluates to itself. t is true.
Integer42, -7Signed 64-bit value.
Float2.5, -0.25, 1.0e3Finite 64-bit floating-point value.
String"Deep Teal"UTF-8 text after escape decoding.
Symboluser/clampResolves through lexical and top-level bindings, built-ins, then commands.
Keyword:deep-tealEvaluates to itself.
URIdither://package/mathValidated first-class URI.
List(+ 1 2)Empty lists are values; non-empty lists are calls or special forms.
Vector[1 2 3]Elements evaluate left to right.
Map{:width 5 :height 7}Keys and values evaluate left to right; order is retained.
FunctionProduced by fn, defn, or function-form defineLexical closure with arity metadata.

Only nil and false are falsey. Empty lists, vectors, maps, zero, and empty strings are truthy.

5. General evaluation grammar

The reader accepts any list of forms. The evaluator interprets a non-empty list using this grammar:

<evaluated-form> ::= <special-form>
                   | <function-call>
                   | <command-call>

<function-call> ::= "(" <operator-expression> <separator>+ <argument-list>? ")"
<operator-expression> ::= <symbol> | <list>
<argument-list> ::= <form> (<separator>+ <form>)*

<command-call> ::= "(" <command-symbol> <separator>+ <command-arguments>? ")"
<command-symbol> ::= <namespaced-symbol>
<namespaced-symbol> ::= <symbol-part> "/" <symbol-part>
<symbol-part> ::= <atom-character>+

Special forms control which arguments evaluate. Functions and commands evaluate arguments from left to right before application.

A vector evaluates each element from left to right and returns a vector. A map evaluates each key followed by its value, entry by entry, and retains entry order.

6. Core special-form grammar

<special-form> ::= <quote-form>
                 | <do-form>
                 | <if-form>
                 | <when-form>
                 | <unless-form>
                 | <and-form>
                 | <or-form>
                 | <let-form>
                 | <for-form>
                 | <for-indexed-form>
                 | <fn-form>
                 | <def-form>
                 | <defn-form>
                 | <define-value-form>
                 | <define-function-form>
                 | <defcustom-form>
                 | <temporary-custom-form>
                 | <try-form>
                 | <require-form>
                 | <module-form>
                 | <provide-form>
                 | <run-form>

<quote-form> ::= "(" "quote" <separator>+ <form> ")"
<do-form> ::= "(" "do" (<separator>+ <form>)* ")"
<if-form> ::= "(" "if" <separator>+ <form>
              <separator>+ <form> (<separator>+ <form>)? ")"
<when-form> ::= "(" "when" <separator>+ <form>
                (<separator>+ <form>)* ")"
<unless-form> ::= "(" "unless" <separator>+ <form>
                  (<separator>+ <form>)* ")"
<and-form> ::= "(" "and" (<separator>+ <form>)* ")"
<or-form> ::= "(" "or" (<separator>+ <form>)* ")"

<let-form> ::= "(" "let" <separator>+ <binding-map>
               <separator>+ <body> ")"
<binding-map> ::= "{" <separator>* <binding-pairs>? <separator>* "}"
<binding-pairs> ::= <binding-name> <separator>+ <form>
                    (<separator>+ <binding-name> <separator>+ <form>)*
<binding-name> ::= <symbol> | <keyword>

<for-form> ::= "(" "for" <separator>+ "[" <symbol>
               <separator>+ <form> "]" <separator>+ <body> ")"
<for-indexed-form> ::= "(" "for-indexed" <separator>+ "[" <symbol>
                       <separator>+ <symbol> <separator>+ <form> "]"
                       <separator>+ <body> ")"

<body> ::= <form> (<separator>+ <form>)*

do, when, unless, and body forms return their last result. An empty do returns nil. if returns nil when its test is falsey and no alternate is present.

and stops at its first falsey value. or stops at its first truthy value. Iteration evaluates its collection once and returns a vector of body results. Lists and vectors iterate in order; maps iterate as [key value] vectors in entry order.

7. Function and definition grammar

<fn-form> ::= "(" "fn" <separator>+ <parameter-form>
              <separator>+ <function-body> ")"
<def-form> ::= "(" "def" <separator>+ <symbol>
               <separator>+ <form> ")"
<defn-form> ::= "(" "defn" <separator>+ <symbol>
                <separator>+ <parameter-form>
                <separator>+ <function-body> ")"
<define-value-form> ::= "(" "define" <separator>+ <symbol>
                        <separator>+ <form> ")"
<define-function-form> ::= "(" "define" <separator>+
                           "(" <symbol> (<separator>+ <parameter>)* ")"
                           <separator>+ <function-body> ")"

<parameter-form> ::= "[" <separator>* <parameters>? <separator>* "]"
                   | "(" <separator>* <parameters>? <separator>* ")"
<parameters> ::= <symbol> (<separator>+ <symbol>)*
               | <symbol>* <separator>* "&" <separator>+ <symbol>
<parameter> ::= <symbol>

<function-body> ::= <docstring>? <separator>* <metadata-header>?
                    <separator>* <body>
<docstring> ::= <string>
<metadata-header> ::= "(" "metadata" <separator>+ <map> ")"

Parameters must be unique. & may appear only before one final rest parameter. Extra arguments are collected into a vector. Closures retain lexical bindings from their definition site while reading current top-level definitions at call time.

8. Custom values

<defcustom-form> ::= "(" "defcustom" <separator>+ <symbol>
                     <separator>+ <form>
                     <separator>+ <custom-metadata> ")"
<custom-metadata> ::= <keyword> <separator>+ <form>
                      (<separator>+ <keyword> <separator>+ <form>)+

<temporary-custom-form> ::= "(" "custom/with-temporary" <separator>+
                            <custom-binding-map> <separator>+ <body> ")"
<custom-binding-map> ::= "{" <separator>* <custom-bindings>?
                         <separator>* "}"
<custom-bindings> ::= <custom-name> <separator>+ <form>
                      (<separator>+ <custom-name> <separator>+ <form>)*
<custom-name> ::= <symbol> | <string>

Every defcustom must include typed metadata, including :type. Metadata keys are unique. Temporary values are validated against the same definition, remain active only for the body, and are restored even when body evaluation fails.

Custom introspection uses ordinary calls:

<custom-call> ::= "(" "custom/get" <separator>+ <custom-name-form> ")"
                | "(" "custom/describe" <separator>+ <custom-name-form> ")"
                | "(" "custom/list" ")"
<custom-name-form> ::= <symbol> | <form>

9. Conditions

<try-form> ::= "(" "try" <separator>+ <try-body>
               (<separator>+ <catch-form>)? ")"
<try-body> ::= <form> (<separator>+ <form>)*
<catch-form> ::= "(" "catch" <separator>+ <symbol>
                 <separator>+ <body> ")"

Only a final catch list is a handler. If the body raises a condition, the catch symbol receives a map containing the condition kind, message, source, details, help URI when available, and recoverability.

10. Modules

<require-form> ::= "(" "require" <separator>+ <package-reference>
                   (<separator>+ <package-reference>)* ")"
<module-form> ::= "(" "module" <separator>+ <package-reference>
                  (<separator>+ <module-field>)* ")"
<provide-form> ::= "(" "provide" <separator>+ <package-reference> ")"

<package-reference> ::= <uri> | <symbol> | <keyword> | <string>
<module-field> ::= <module-key> <separator>+ <form>
<module-key> ::= ":title" | ":version" | ":summary" | ":requires"
               | ":exports" | ":capabilities" | ":metadata"

A non-URI package reference resolves under dither://package/. :title, :version, and :summary take strings. :requires takes one reference or a list/vector of references. :exports and :capabilities take one name or a list/vector. :metadata takes a map. provide must name the current module.

11. Commands

The canonical command form is:

<run-form> ::= "(" "run" <separator>+ <command-uri>
               (<separator>+ <command-argument-form>)? ")"
<command-uri> ::= <uri>
<command-argument-form> ::= <map> | <keyword-arguments>
<keyword-arguments> ::= <keyword> <separator>+ <form>
                        (<separator>+ <keyword> <separator>+ <form>)*

A command exported as a symbol may use direct-call syntax:

; Compile the open project with its saved machine and material settings.
(project/compile)

Argument schemas, not the grammar, determine which fields are required and which value kinds are valid. See the generated command reference for the active catalog.

12. Built-in call families

The following names are ordinary strict calls. Their arguments evaluate left to right before the operation runs.

FamilyNames
Type testsnil?, bool?, number?, int?, float?, string?, symbol?, keyword?, uri?, list?, vector?, map?, function?, command?, job?, handle?
Comparison=, not=, <, <=, >, >=, not
Arithmetic+, -, *, /, mod, abs, min, max, round, floor, ceil, int, float, range, exp, sqrt, sin, cos, atan2
Collectionscount, empty?, first, rest, nth, get, get-in, has-key?, assoc, dissoc, update, keys, values, map, filter, reduce
Stringsstr, string/contains?, string/starts-with?, string/ends-with?, string/trim, string/split, string/join
URIsuri/join, uri/space, uri/kind, uri/path, uri/parent, uri/name, uri/resolve, uri/canonical
Diagnosticsprint, log, warn, debug
Conditionscondition/kind, condition/message, condition/help, condition/source, condition/details, condition/recoverable?
Functionsfunction/name, function/params, function/rest, function/doc, function/metadata, function/arity
Modulesmodule/current, module/requires

Installed modules may add names such as vector/bezier or embroidery/export. Added functions obey ordinary call syntax and cannot introduce new token or delimiter rules.

13. Numeric and collection bounds

Synchronous evaluation has a 4,096-step budget. Bounded range, iteration, recursion, and nested function calls consume that budget. Crossing it raises a recoverable resource-limit condition instead of blocking Satin Studio.

range uses an exclusive end:

; Produce 0, 2, 4, and 6; the end value 8 is excluded.
(range 0 8 2)

Division returns a float. Integer operations retain integers when possible. Arithmetic overflow, division by zero, modulo by zero, invalid transcendental domains, non-finite results, and out-of-range conversion to int raise arithmetic conditions.

14. Determinism

Within one language and package version:

Wall-clock time, unordered host maps, hidden random seeds, and platform-specific tie breaking must not affect deterministic project output.

15. Source locations and errors

Each top-level parsed form records source URI when supplied, byte range, line range, and column range. A condition identifies a kind and message and may include source, details, a related Help URI, and a recoverable flag.

Reader errors include invalid URI, invalid number, unexpected end, unexpected closing delimiter, unterminated string, and odd map entry count. Evaluator conditions include invalid form, invalid binding, arity, type mismatch, unbound symbol, arithmetic, resource limit, command validation, command availability, and package-defined failures.

16. Complete example

The two-part Recursive Firebird program exercises the reader grammar, custom values, functions, iteration, modules, deterministic seeds, command metadata, and package calls. Continue with part 2, which performs negative-space compilation, perceptual optimization, five-thread routing, verification, preview, save, and export.

The step-by-step Firebird walkthrough explains every numbered section and distinguishes core Dither forms from the embroidery, geometry, rendering, and job operations supplied by installed modules.