1. Language

Table of Contents

1.1. Syntax ↑

The .dust format extends .ron. References, invocations, quotation shorthand, and mute shorthand belong exclusively to DUST. Their explicit tagged-element representations are ordinary data in RON and JSON.

It is conventional for Lisps to have a reader, the system that ingests plain text and outputs abstract-syntax-trees used by the compiler and macros. DUST doesn't have a reader in the traditional Lisp sense, rather it has a parser1.

DUST builds on RON’s data notation and adds its own expression parser. Parsing constructs forms without evaluating them.

Parentheses delimit invocations, not literal lists or quotation forms.

[square brackets] are also used to delimit lists, used for literal data structures and in specials and macros to delimit where new bindings are introduced, such as argument lists and let bindings.

{curly braces} delimit maps containing key-value pairs and optional tags. Ordinary members pair a string key with a value. Tags supply boolean-valued members using RON's shorthand. Map keys must be strings. Map values can be of any type.

1.1.1. Strings ↑

DUST uses RON string syntax: bare strings, double-quoted strings, and fences of two or more double quotes. All produce strings, not quotation forms. The examples below show the RON data produced by parsing DUST source.

In value position, a bare token matching a number, null, true, or false denotes that value. Quote it to make a string. Other bare tokens denote strings, except where DUST reserves a prefix for expression syntax.

A leading # introduces a tagged element. In value position, a leading . introduces a local reference, a leading : a constant reference, and a leading / a JSON Pointer. Leading `, ~, ,, and ,@ introduce quotation forms. A leading \ mutes the next complete form. Quote these characters when they are meant to begin a string. Apostrophes and printable Unicode characters, including emoji, are ordinary string characters:

DUST
[hello ".-#1" don't true false null
 42 -1.5 "true" "false" "null" "42"
 "-1.5" nil "#name" "^name" .name
 🙂 "" {true false}]
→
Canonical DUST
hello
".-#1"
don't
true
false
null
42
-1.5
"true"
"false"
"null"
"42"
"-1.5"
nil
"#name"
^name
.name
🙂
""
<!true>
→
RON
[hello .-#1 don't true false
 null 42 -1.5 "true" "false"
 "null" "42" "-1.5" nil "#name"
 ^name #local name 🙂 "" <!true>]

A square bracket alone on its opening line starts a line-oriented list. A bare string item occupies one line and may contain spaces; numeric and other scalar literals keep their types. If the first item follows [ on the opening line, whitespace separates items on every line; quote strings containing spaces in that packed form:

DUST
[
  Ada Lovelace
  Grace Hopper
]
→
Canonical DUST
"Ada Lovelace"
"Grace Hopper"
→
RON
[
  Ada Lovelace
  Grace Hopper]

Ordinary double-quoted strings use JSON escapes. Fenced strings allow double quotes inside without escaping them, but backslashes and control characters still use escapes. Unicode escapes decode to their characters:

DUST
["say \"hello\"" ""C:\\tmp\\notes.txt"" "\u03b5" "\u0000"]
→
Canonical DUST
""say "hello"""
C:\tmp\notes.txt
ε
"\u0000"
→
RON
[
  say "hello"
  C:\tmp\notes.txt
  ε
  "\u0000"]

A fence of three or more quotes followed only by whitespace and a newline starts a multiline string. The newline after the opening fence and the newline before the closing fence are not part of the string. An extra blank line before the closing fence preserves a trailing newline. A single-line fence and a multiline fence can therefore contain the same string:

DUST
[
  ""hello world""
  """
    hello world
  """
  """
    first line
    second line
  """
]
→
Canonical DUST
"hello world"
"hello world"
"""
  first line
  second line
"""
→
RON
[
  hello world
  hello world
  """
    first line
    second line
  """
]

A quoted ., :, or / is a string; an unquoted one starts a reference or pointer. A caret has no special meaning:

DUST
["^name" "^x" .name :name "^" .name]
→
Canonical DUST
^name
^x
.name
:name
^
.name
→
RON
[^name ^x #local name #const name ^ #local name]

Outside map-key position, a token beginning with . or : denotes a reference. Outside map-key and direct-callee positions, a token that starts with / is a pointer. Put quotes around the token to make it a string. Binding forms introduce names as strings, conventionally written bare. A direct callee is a name encoded as a string in the first element of #invoke, without a #local or #const wrapper. In this position, surrounding quote characters belong to the name. A computed callee is an expression in the first element of #invoke, including a reference such as (.f 1) or (:strings/case upper hi). Elsewhere, bare names and quoted strings denote text.

Whitespace and layout follow RON, except for DUST's form separation rules.

1.2. Source notation ↑

1.2.1. Documents and form separation ↑

A DUST file holds one or more forms. A file containing exactly one form is that form. Two or more forms, separated by whitespace including newlines, make an implied root list; its outer square brackets may be written or omitted. A root string containing spaces must be quoted. Unlike RON, whose default document is an implied outer object, DUST has no implied root map: a root map is written with braces.

Canonical DUST omits the root list's brackets when the list has two or more forms and its first non-comment form is not a literal list:

(+ 1 2)
(* 3 4)

Root forms are separated by whitespace, not lines, so a root string containing spaces is quoted, as in "two words".

Explicit outer brackets around these forms are also accepted and produce the same list, but are not canonical.

DUST
(+ 1 2)
(* 3 4)
→
RON
[#invoke [+ 1 2] #invoke [* 3 4]]
=

Otherwise the brackets are kept, so a one-element list [x] stays distinct from the form x, and [[1 2]] stays distinct from [1 2]. Once an earlier form establishes an implied root list, later literal lists need only their own brackets.

DUST
[[1 2]]
→
RON
[[1 2]]
=
DUST Result
[[1 2]]
DUST
1
[2 3]
→
RON
[1 [2 3]]
=
DUST Result
1
[2 3]

A file is evaluated one top-level form at a time, from first to last, so a form can use the top-level locals that earlier forms define. Its value is the list of the forms' values: (+ 1 2) (* 3 4) evaluates to [3 12], and a file with one form evaluates to that form's value. An empty file is an empty list. Read for evaluation, as by the Epsilon REPL, a written list is always one form, so [1 2] followed by [3] is two forms, and each form can have a depth of 256.

DUST comments start with ; at the start of a line or after whitespace and continue to the end of that line. A semicolon inside a quoted string or inside an unbroken token is text. RON uses the same comment syntax. A document must not mix ; comments with an explicit $comment member.

A comment belongs to the value occurrence it describes. A comment immediately before an item belongs to that item. A blank line between the comment and item attaches the comment to the enclosing collection. A comment after a value on the same line belongs to the end of that occurrence.

For example, this file evaluates to 3:

; Root documentation
(+ 1 2)

In explicit comment round-trip mode, its JSON representation is:

[
  {"$comment": "{\"root\":\"scalar\",\"start\":{\"\":[\"Root documentation\"]}}"},
  {"#invoke": ["+", 1, 2]}
]

The marker preserves the comment and is not an element of the value. Collection opening and closing comments are distinct from comments on the collection occurrence. epsilon convert --comment-roundtrip preserves these placements when converting through JSON.

In expression positions, unquoted (), [], and {} delimit forms and end bare strings, reference names, and direct-callee names. In packed lists and invocations, these delimiters separate forms without whitespace.

DUST
[[foo(bar)]
 (foo(bar))
 [foo[bar]{}(bar)baz]
 [[.foo(bar)][(bar):foo]]]
→
Canonical DUST
[[foo (bar)] (foo (bar)) [foo [bar] {} (bar) baz] [[.foo (bar)] [(bar) :foo]]]
→
RON
[[foo #invoke [bar]]
 #invoke [foo #invoke [bar]]
 [foo
  [bar]
  {}
  #invoke [bar]
  baz]
 [[#local foo #invoke [bar]] [#invoke [bar] #const foo]]]

Each line-oriented list item or map member value contains one form. Bare line strings can contain spaces, but end at expression delimiters. foo(bar) contains two forms. It may supply consecutive root forms, but is invalid as one line-oriented list item or map member value. "foo(bar)" is one string.

Map keys use RON's token rules, including embedded brackets and parentheses, and require whitespace before their values. Angle brackets delimit tag groups only in map-member position.

DUST
[{items[0] 1 foo(bar) 2 .name 3}]
→
Canonical DUST
[{.name 3 foo(bar) 2 items[0] 1}]
→
RON
[{.name 3
  foo(bar) 2
  items[0] 1}]

A key that begins with ) is quoted, since a line that begins with ) closes a form:

DUST
{"(a" b ")" 1 ")x" 2 "x)" 3 "(" 4 k ["a <b" "c" "a<b"]}
→
Canonical DUST
{( 4
 (a b
 ")" 1
 ")x" 2
 k [
   "a <b"
   c
   a<b]
 x) 3}
→
RON
( 4
(a b
) 1
)x 2
k [
  "a <b"
  c
  a<b]
x) 3

1.2.2. Nesting depth ↑

Forms can nest to a maximum depth of 256. The parser and the evaluator count depth with the same rules:

  • Each list, map, invocation, tag, quotation prefix, and mute prefix adds one level. A scalar adds no level, so [[1]] has a depth of 2.
  • An invocation is one level, because its list is a part of the invocation.
  • A list that is the payload of a tag or of a prefix is a part of it and adds no level. Thus (f x), #invoke [f x], {#invoke [f x]}, and `[x] each have a depth of 1.
  • A form in a string template or in a JSON pointer is below the template or the pointer.
  • A root list adds one level, written or implied. Thus each form of a file with two or more forms can have a depth of 255. A file with one form has no root list, and that form can have a depth of 256.

The parser rejects a form that is deeper than 256 levels. This is a resource limit, not a syntax error. The result is #err <limit resource>, with a message that gives the depth limit:

#err {<limit resource>
      column 257
      line 1
      message "DUST parse: nesting exceeds the depth limit of 256"}

Other parser errors produce #err <invalid value>. RON documents have the same depth limit and count levels with the same rules.

1.2.3. Internal limits ↑

An implementation can have internal limits that are lower than the limits of the language. These limits are high-water marks of the implementation, not of the language. A valid program can be outside an internal limit. For example, a valid program can have expressions that nest too deeply for the implementation, or a function that has too many names. An implementation can find a limit before evaluation starts or during evaluation. For example, calls that nest too deeply during evaluation are outside the recursion limit. The result of such a program is #err <limit internal KIND>, where KIND names the limit. The message starts with "program exceeds an internal limit:" and names the limit.

Tags Limit Message
<internal limit nesting> Nesting of expressions program exceeds an internal limit: expressions nest too deeply
<internal limit locals> Names in one function program exceeds an internal limit: too many names in one function
<captures internal limit> Names that one function captures program exceeds an internal limit: a function captures too many names
<internal limit stack> Working slots of one function program exceeds an internal limit: a function needs too many working slots
<internal limit size> Size of one function body program exceeds an internal limit: a function body is too large
<constants internal limit> Constants in one function program exceeds an internal limit: a function has too many constants
<internal limit recursion> Nesting of calls during evaluation program exceeds an internal limit: calls nest too deeply

A different implementation, or a later version, can accept the same program. Thus a result outside an internal limit is not a part of the language contract.

DUST
[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[1]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]
→
Canonical DUST
[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[
                                                                                                                                                                                                                                                                 1]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]
=
DUST Result
#err {<internal limit nesting>
      message "program exceeds an internal limit: expressions nest too deeply"}
DUST
(let [count-up (fn count-up [n] (+ 1 (count-up (+ .n 1))))] (count-up 0))
=
DUST Result
#err {<internal limit recursion>
      message "program exceeds an internal limit: calls nest too deeply"}

1.2.4. References ↑

Outside map-key position, . followed by a nonempty name produces a #local tagged element, and : followed by a nonempty name produces a #const tagged element. The two are the same in the first position of an invocation, where they make a computed callee. Names follow RON's packed bare-token rules and DUST's form separation rules above. Numeric, boolean, and null spellings are names after the prefix, so .5 is #local "5", not a number. An empty name, or a name that begins with a quote, ., or :, is invalid: ., :, ..., ::x, and ."x" are syntax errors. Quote such text to write a string. A . or : after the first character of a bare string is text, as in a.b or a:b.

DUST
{var .name
 const :name
 punctuation (fn? :+)
 qualified-var .user/name
 qualified-const :math/pi
 colon .a:b
 dot :a.b
 literal-names [.42 .-1 .1.5 .true .false .null]
 caret-name .^+
 nested [.x {value :y}]}
→
Canonical DUST
{caret-name .^+
 colon .a:b
 const :name
 dot :a.b
 literal-names [.42 .-1 .1.5 .true .false .null]
 nested [.x {value :y}]
 punctuation (fn? :+)
 qualified-const :math/pi
 qualified-var .user/name
 var .name}
→
RON
caret-name #local ^+
colon #local a:b
const #const name
dot #const a.b
literal-names [#local "42" #local "-1" #local "1.5" #local "true" #local "false" #local "null"]
nested [#local x
        {value #const y}]
punctuation #invoke [fn? #const +]
qualified-const #const math/pi
qualified-var #local user/name
var #local name
=
DUST Result
#err <name unbound>
DUST
(let [name Ada] .name)
→
RON
#invoke [let [name Ada] #local name]
=
DUST
:name
→
RON
#const name
→
Evaluation cases
#cases [{bindings {name Ada} output Ada}]
DUST
(let [add :+]
  [(add 0 0)
   (add 1 2)
   (add -4 10)])
→
Canonical DUST
(let [add :+] [(add 0 0) (add 1 2) (add -4 10)])
→
RON
#invoke [let [add #const +] [#invoke [add 0 0] #invoke [add 1 2] #invoke [add -4 10]]]
=
DUST
:42
→
RON
#const "42"
→
Evaluation cases
#cases [{bindings {42 42} output 42}]
DUST
:true
→
RON
#const "true"
→
Evaluation cases
#cases [{bindings {true enabled} output enabled}]
DUST
:null
→
RON
#const "null"
→
Evaluation cases
#cases [{bindings {null missing} output missing}]

Parsing constructs the reference without looking up its name. Binding and parameter names in DUST are ordinary strings, conventionally written bare, rather than references. Parsing does not interpret binding positions.

1.2.5. String Template Literals ↑

A string template is a quoted or fenced string prefixed with ~, the quasiquote prefix. Inside it, , followed by a form embeds that form, as unquote does in a quasiquoted list. A string without ~ is always literal, so ",.name" and "{{x}}" are plain text.

(let [name Ada] ~"Hello ,.name")                    ; => "Hello Ada"
(let [subtotal 10 tax 2] ~"Total: ,(+ .subtotal .tax)") ; => "Total: 12"
~"Pi is ,:math/pi"                                  ; => "Pi is 3.141592653589793"
(let [name Ada]
  ~"""
    Hello ,.name
    Sum ,(+ 1 2)
    """)                                            ; => "Hello Ada\nSum 3"

An embedded form in parentheses, brackets, braces, or quotes ends at its closing delimiter. Any other form, such as a reference, reads a full name and ends at whitespace, a comma, a quote, or a bracket. A comma that ends it is consumed, so a reference can be followed directly by text:

(let [name Ada] ~"Hi ,.name there")     ; => "Hi Ada there"
(let [name Ada] ~"Hi ,.name,!")         ; => "Hi Ada!"
(let [foo! bang] ~"Hi ,.foo!")          ; => "Hi bang"
(let [name Ada] ~"Hi ,.name,, there")   ; => "Hi Ada, there"

A comma followed by whitespace or the closing quote is text, and ,, writes a literal comma anywhere:

~"a, b"      ; => "a, b"
~"x,,y"      ; => "x,y"

Embedded forms may contain quotes, as in ~"a ,(str "x") b". A quoted string cannot be embedded directly in a one-line template, because ," before the closing quote is literal text and a quote otherwise ends the template; write ,(str "x") or use a fenced ~""" template. Only a string written directly after ~ is a template; a string nested inside a quasiquoted list stays literal, so ~[a ,.x "b ,.x"] keeps "b ,.x".

The resulting template value keeps the literal pieces and evaluated values separate, so template functions can inspect them or escape each value. A printed or exported template value is its rendered string, the same text str produces. Printing loses the parts: reading the printed result back gives an ordinary string, not a template. A quoted template is a form rather than an evaluated value, so it prints as a template literal.

In RON, the #template tagged representation contains a list of literal strings and a list of forms. The first list has one more item than the second. For example, this DUST template:

~"hello-,.name"

has this RON representation:

#template [[hello- ""] [#local name]]

DUST formatting writes the template literal when the tagged representation can be expressed without changing its data. It always writes a one-line ~"…" literal, escaping line breaks as \n, so a multiline ~""" template is reformatted on one line. A template that embeds a string value that is not a bare token keeps the explicit #template notation.

See string templates for construction, inspection, and rendering.

1.2.6. JSON Pointers ↑

RFC 6901 defines JSON Pointer as a string syntax for identifying a value within a JSON document. Its reference tokens are separated by /; within a token, ~1 represents / and ~0 represents ~. The empty string refers to the root, while / refers to a member with an empty name.

DUST uses that syntax for pointer text and adds pointer values and embedded forms. A pointer value can be used with DUST maps and lists. DUST distinguishes integer index segments from string key segments; numeric strings with valid index spelling become indices. Embedded forms and DUST's backslash escapes below are DUST extensions, not RFC 6901 pointer text.

(get {a/b 1} /a~1b) ; 1
(get {items [zero one]} /items/1) ; one
(get {"" 7} /) ; 7

A slash-prefixed value constructs a pointer. Pass it to a collection path function to read or update data. In callee position, / is division; a map literal key beginning with / is a string.

(get {a 1} /a) ; 1
(/ 10 2)       ; 5.0
(get {/a 1} "/a") ; 1

A pointer has slash-separated segments:

(get {"" 7} /)                 ; 7
(get {"" {a 1}} //a)           ; 1
(get {a {"" 2}} /a/)           ; 2
(get {a 1} (jsonptr ""))       ; {a 1}

/ selects an empty key; it does not select the root. Use (jsonptr "") for the root pointer.

Segments can contain embedded forms. As in a string template, , followed by a form embeds it, and a comma that ends a token form is consumed. A pointer needs no ~: a bare pointer cannot otherwise contain a comma.

(let [key a] (get {a 1} /,.key))                   ; 1
(let [id 2] (get {user-3 ok} /user-,(+ 1 .id)))    ; ok
(let [id 7] /users/,.id,/name)                     ; /users/7/name

Names can contain /, so /users/,.id/name reads the reference .id/name; the closing comma in /users/,.id,/name ends .id before the next segment. An embedded segment value must be a string or a nonnegative integer.

The RFC escapes ~1 and ~0 select keys containing / and ~:

(get {a/b 1} /a~1b) ; 1
(get {m~n 2} /m~0n) ; 2

DUST also accepts \{, \}, and \\ for literal braces and backslashes, and ,, for a literal comma. Other tilde or backslash escapes are syntax errors. Parentheses, caret, opening braces, and double quote are literal pointer text; an unmatched closing parenthesis ends the pointer token. Whitespace, square brackets, a closing brace, or a comma followed by whitespace or a closer also ends the token.

RON represents a literal pointer as #jsonptr with a string payload. A pointer containing embeds has a list payload. For example, these DUST pointers:

/a/0
/3/,"3"
/foo/user-,(+ 1 .id)/,.k,/0

have these RON representations:

[#jsonptr /a/0 #jsonptr /3/3 #jsonptr [foo [user- #invoke [+ 1 #local id]] #local k 0]]

Formatting the third pointer as DUST writes its embedded forms in the pointer literal rather than the #jsonptr representation.

In the second example, both segments are indices. Map lookup converts an index to its decimal string key. Quotation, mute, and unevaluated parts of quasiquotation preserve tagged data; unquote follows the usual quasiquote rules.

DUST
(jsonptr? /)
(str /)
(str //a)
(str /a/)
(str /a~1b/x~0y)
(str /f(x))
(let [k name] (str /users/,.k))
(let [n 10] (str /user-,(+ .n 1)))
(let [k "a/b"] (str /,.k))
(let [k "x~y"] (str /,.k))
(let [p /3/,"3"] (get [null null null {3 found}] .p))
(let [p /3/,(int "3")] (get [null null null [a b c found]] .p))
/,null
/,true
(count /a/b/0)
(first /a/b/0)
(str (rest /a/b/0))
(= /a/0 (jsonptr "/a/0"))
(let [x 3] (str /foo/,.x))
(get [a b c d] /,"3")
(get [a b c d] /,"\u0033")
(let [n "3"] (get [a b c d] /,.n))
(let [n "3"] (get {3 yes} /,.n))
(let [n "03"] (get {03 yes} /,.n))
(let [n "3"] (= /,.n /3))
→
Canonical DUST
(jsonptr? /)
(str /)
(str //a)
(str /a/)
(str /a~1b/x~0y)
(str /f(x))
(let [k name] (str /users/,.k))
(let [n 10] (str /user-,(+ .n 1)))
(let [k a/b] (str /,.k))
(let [k x~y] (str /,.k))
(let [p /3/3] (get [null null null {3 found}] .p))
(let [p /3/,(int "3")] (get [null null null [a b c found]] .p))
/,null
/,true
(count /a/b/0)
(first /a/b/0)
(str (rest /a/b/0))
(= /a/0 (jsonptr "/a/0"))
(let [x 3] (str /foo/,.x))
(get [a b c d] /3)
(get [a b c d] /3)
(let [n "3"] (get [a b c d] /,.n))
(let [n "3"] (get {3 yes} /,.n))
(let [n 03] (get {03 yes} /,.n))
(let [n "3"] (= /,.n /3))
=
DUST Result
true
"/"
"//a"
"/a/"
"/a~1b/x~0y"
"/f(x)"
"/users/name"
"/user-11"
"/a~1b"
"/x~0y"
found
found
#err {<mismatch type>
      message "pointer segments must be strings or integers"}
#err {<mismatch type>
      message "pointer segments must be strings or integers"}
3
a
"/b/0"
true
"/foo/3"
d
d
d
yes
yes
true
DUST
(let [p /3/,"3"]
 [.p (get [null null null {3 yes}] .p) (str .p)])
(let [name Ada] (str /,~"hello-,.name"))
(str /,~"inner ,(str "x")")
→
Canonical DUST
(let [p /3/3] [.p (get [null null null {3 yes}] .p) (str .p)])
(let [name Ada] (str /,~"hello-,.name"))
(str /,~"inner ,(str x)")
=
DUST Result
[
  [/3/3 yes "/3/3"]
  "/hello-Ada"
  "/inner x"]

1.2.7. Invocations ↑

Parentheses produce a #invoke tagged element whose payload is a list. The first element is the callee, and the remaining elements are argument forms. A direct callee is a name encoded as a string, without a #local or #const wrapper. In this position, surrounding quote characters belong to the name. This differs from string parsing in argument position. A callee that begins with . or : is a reference, and therefore a computed callee.

DUST
(upper hello)
→
RON
#invoke [upper hello]
→
Evaluation cases
#cases [{bindings {upper (fn [x] .x)} output hello}]
DUST
("upper" hello)
→
RON
#invoke ["""
           "upper"
         """
         hello]
→
Evaluation cases
#cases [{bindings {""\"upper""" (fn [x] .x)} output hello}]
DUST
(+ 1 2)
→
RON
#invoke [+ 1 2]
=
DUST
("+" 1 2)
→
RON
#invoke ["""
           "+"
         """
         1
         2]
→
Evaluation cases
#cases [{bindings {""\"+""" (fn [a b] (+ .a .b))} output 3}]
DUST
(let [twice (fn [x] (* 2 .x))]
  (.twice 21))
→
Canonical DUST
(let [twice (fn [x] (* 2 .x))] (.twice 21))
→
RON
#invoke [let [twice #invoke [fn [x] #invoke [* 2 #local x]]] #invoke [#local twice 21]]
=
DUST
(:upper hello)
→
RON
#invoke [#const upper hello]
→
Evaluation cases
#cases [{bindings {upper (fn [x] .x)} output hello}]
DUST
(:+ 1 2)
→
RON
#invoke [#const + 1 2]
=
DUST
(f :x)
→
RON
#invoke [f #const x]
→
Evaluation cases
#cases [{bindings {f (fn [value] .value) x Ada} output Ada}]
DUST
[upper hello]
→
Canonical DUST
upper
hello
→
RON
[upper hello]
=
DUST Result
upper
hello

A computed callee is encoded as the first form in #invoke. For example:

DUST
((if true :+ :*) 2 3)
→
RON
#invoke [#invoke [if true #const + #const *] 2 3]
=

The parser does not resolve invocation names, check whether a callee produces a function, or evaluate arguments. Unknown names and expressions such as (/ 1 0) are valid parser inputs. Their evaluation belongs to DUST.

An empty invocation, (), parses as #invoke []. Parsing does not require a callee; an empty invocation has no function to evaluate. Whitespace separates forms throughout an invocation, including across newlines, even when the callee begins on a later line. A bare argument containing spaces must be quoted. Lists retain RON's packed and line-oriented rules in every position, including binding and parameter lists. The parser does not interpret special-form names or binding positions.

DUST
()
→
RON
#invoke []
DUST
(
  .f
  hello world
)
→
Canonical DUST
(.f hello world)
→
RON
#invoke [#local f hello world]
DUST
(let [
  first name
  second name
] [first name])
→
Canonical DUST
(let
  [
    first name
    second name]
  [first name])
→
RON
#invoke [let
         [
           first name
           second name]
         [first name]]

1.2.8. Quotation forms ↑

The four prefixes apply to the immediately following form and parse to a tagged element. The prefix and its form are adjacent; quote or fence a string beginning with one of these characters when it is meant as text.

Prefix Explicit tagged notation
`form #quote form
~form #quasiquote form
,form #unquote form
,@form #unquote-splicing form
DUST
`.name
→
RON
#quote #local name
=
DUST
~[a .b]
→
RON
#quasiquote [a #local b]
=
DUST
,.name
→
RON
#unquote #local name
=
DUST Result
#err <outside quasiquote unquote>
DUST
,@.items
→
RON
#unquote-splicing #local items
=
DUST Result
#err <outside quasiquote splicing unquote>
DUST
`~(f ,.x ,@.xs)
→
RON
#quote #quasiquote #invoke [f #unquote #local x #unquote-splicing #local xs]
=
DUST Result
~(f ,.x ,@.xs)

Quoting a string does not turn it into a DUST quotation form.

DUST
#quote .name
→→
RON
#quote #local name
=
DUST
#quasiquote [a b]
→→
RON
#quasiquote [a b]
=
DUST
#unquote .name
→→
RON
#unquote #local name
=
DUST Result
#err <outside quasiquote unquote>

Quotation names have no special meaning inside parentheses: (quote 1) parses as #invoke [quote 1] and formats as (quote 1). The explicit quotation notation is #quote 1, whose canonical DUST spelling is `1. The same rules apply to the other three quotation tags.

Quote returns its enclosed form as data without evaluating references, invocations, or collection contents within it. Quasiquote constructs data in the same way, except that unquote evaluates one enclosed form and substitutes its resulting value. Unquote-splicing evaluates an enclosed list and inserts its elements into the surrounding list, in order.

With x bound to 2 and xs bound to [3 4]:

`[a .x]           ; returns [a .x]
~[a ,.x ,@.xs]    ; returns [a 2 3 4]

Nested quasiquotes match unquotes by nesting depth: entering a quasiquote increases the depth, and an unquote or splice belongs to its matching quasiquote. An inner quasiquote's substitutions are not executed by an outer one. Unquote outside quasiquote produces #err <outside quasiquote unquote>, and unquote-splicing outside quasiquote produces #err <outside quasiquote splicing unquote>. Unquote-splicing outside a list-element position, or splicing a value that is not a list, produces #err <invalid quotation>.

The DUST parser constructs the tagged elements. DUST decides how to resolve a reference, invoke a function, or process a quotation form. Those evaluation rules belong to DUST rather than the data format.

1.2.9. Application value prefixes ↑

The meanings of #link, #dur, #utc, and #hash belong to DUST. They use RON’s generic #prefix value notation; RON accepts these prefixes without interpreting them. Payload validation and evaluation semantics belong to DUST. Parsing preserves the tagged representation without evaluating it, except that a valid #utc or #dur payload is rewritten to its canonical spelling: #utc 1970-01-01T00:00Z reads and formats as #utc 1970-01-01T00:00:00Z, and #dur PT90M as #dur PT1H30M. Payloads that do not parse are kept for evaluation to reject.

Epsilon imports #utc as a typed UTC value. Evaluating it returns that same value; it does not execute or reinterpret its timestamp payload. Formatting exports the normal tagged representation through the selected notation codec.

#utc and #dur are distinct signed 64-bit counts of 100 ns ticks. UTC counts from the Unix epoch; durations count elapsed ticks. Canonical text has at most seven fractional second digits. Input accepts up to nine digits, rounded to the nearest tick, with halfway cases rounded away from zero. A nonzero timestamp or duration that would round to zero is represented by one tick in magnitude. See the Time library for ranges, constructors, units, calendar operations, and explicit tick/nanosecond conversion.

#link constructs a distinct link value with an integer, string, or tagged payload. Integer payloads use the full signed 64-bit range. String payloads stay strings, including numeric-looking names such as #link "42". Tagged payloads retain their tag and value, as in #link #url https://arst.com/arst.

Bare strings are valid: #link foo carries the name foo. Epsilon preserves names without resolving them; a query or transaction engine can interpret a name as a temporary ID. Epsilon does not fetch or interpret tagged link targets.

A reference or invocation in the payload is evaluated before constructing the link. For example, (let [foo 42] #link .foo) produces #link 42. Quotation preserves constructor syntax: `#link .foo does not look up foo. Booleans, floats, plain maps, and lists are invalid evaluated link payloads. A payload that evaluates to an error is the result instead of a link: #link .missing returns the unbound-name error, so error? and falsiness see it.

DUST
#link .missing
#link (/ 1 0)
(error? #link .missing)
=
DUST Result
#err <name unbound>
#err <division integer zero>
true

The canonical form #link 42 has this JSON representation:

{"#link": 42}

A link is distinct from an integer even when their payloads match. Both (int? #link 42) and (number? #link 42) return false, and numeric operations reject link operands. Integer link payloads sort before string payloads, then tagged payloads. Payloads use their native ordering; tagged ordering requires scalar tag payloads. Comparing #link 42 with the integer 42 produces a type-mismatch error.

DUST
(do [#link 0 #link -1 #link -9223372036854775808 #link 9223372036854775807])
=
DUST Result
#link 0
#link -1
#link -9223372036854775808
#link 9223372036854775807
DUST
#link "42"
=
DUST Result
#link "42"
DUST
(int? #link 42)
=
DUST
(number? #link 42)
=
DUST
(+ 1 #link 42)
=
DUST Result
#err <mismatch type>
DUST
(do [(= #link 42 #link 42) (= #link 42 #link 43) (comparison/identical? #link 42 #link 42)])
=
DUST Result
true
false
true
DUST
(= #link 42 42)
=
DUST Result
#err <mismatch type>
DUST
{#utc 2026-10-24T12:34:56Z}
→
Canonical DUST
#utc 2026-10-24T12:34:56Z
=
DUST Result
#utc 2026-10-24T12:34:56Z
DUST
{#utc 2026-10-24T12:34:56Z}
→
Canonical DUST
#utc 2026-10-24T12:34:56Z
→
RON
#utc 2026-10-24T12:34:56Z
=
DUST Result
#utc 2026-10-24T12:34:56Z
DUST
{
  dependsOn {#link 42}
  when {#utc 2026-09-09T00:00:00Z}
}
→
Canonical DUST
{dependsOn #link 42
 when #utc 2026-09-09T00:00:00Z}
→
RON
dependsOn #link 42
when #utc 2026-09-09T00:00:00Z
=
DUST Result
{dependsOn #link 42
 when #utc 2026-09-09T00:00:00Z}
DUST
#dur PT30S
→
RON
#dur PT30S
DUST
#utc 2026-10-24T12:34:56Z
→
RON
#utc 2026-10-24T12:34:56Z

#hash holds a digest tagged with its algorithm; the hash library defines its payload and equality.

DUST
#hash #sha256 e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
→
RON
#hash #sha256 e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
=
DUST Result
#hash #sha256 e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855

1.2.10. Mute ↑

A backslash prefix, \form, mutes the immediately following complete form. The prefix and its form are adjacent. It applies to invocations, references, scalar values, lists, maps, and tagged elements. The parser still reads the muted form's notation and balances its delimiters; malformed strings or unmatched delimiters remain syntax errors.

A muted form is represented as a #mute tagged element whose payload is the preserved form. For example, (+ 1 \(* 20 30) 2) has the form representation #invoke [+ 1 #mute #invoke [* 20 30] 2]. The explicit #mute form spelling is accepted, and canonical DUST writes the backslash shorthand. RON and JSON keep the tagged element.

DUST
(+ 1 #mute (* 20 30) 2)
→
Canonical DUST
(+ 1 \(* 20 30) 2)
→
RON
#invoke [+ 1 #mute #invoke [* 20 30] 2]
DUST
#mute #invoke [+ 1 2 3]
→
Canonical DUST
\(+ 1 2 3)
→
RON
#mute #invoke [+ 1 2 3]
=

When reading files or interpreting source lists and invocation argument sequences, DUST excludes muted elements before evaluation, macro expansion, and arity or binding checks. Their payloads are not evaluated or checked for semantic validity. Muted elements contribute no element or argument, not a null placeholder. The active forms must satisfy the usual rules. Known tags inside a muted payload are still validated on import, so \#local 42 is invalid just as #local 42 is.

(+ 1 \(* 2 3) 4)        ; evaluates to 5
DUST
(+ 1 #mute (/ 1 0) 2)
→
Canonical DUST
(+ 1 \(/ 1 0) 2)
=
DUST
[1 #mute .missing 2]
→
Canonical DUST
1
\.missing
2
=
DUST
[\.missing \[1 2] \{a 1}]
→
Canonical DUST
\.missing
\[1 2]
\{a 1}
→
RON
[#mute #local missing
 #mute [1 2]
 #mute {a 1}]
=

A file containing only muted forms produces no evaluation results. A muted value cannot be compiled directly as a root expression or supply a map value or invocation callee. These uses are evaluation errors, not parse errors: #err <mute root value>, #err <map mute value>, and #err <callee mute>. Runtime data is not filtered for mute elements.

DUST
#invoke [#mute + 1 2]
→=
DUST Result
#err <callee mute>

To begin a literal string with a backslash, quote the string.

DUST
["\\path" "\\(+ 1 2)"]
→
Canonical DUST
"\\path"
"\\(+ 1 2)"
→
RON
[
  \path
  \(+ 1 2)]
=
DUST Result
"\\path"
"\\(+ 1 2)"

1.2.11. Canonical expression formatting ↑

Canonical DUST formatting preserves the parsed form. Consecutive forms in an invocation or packed list are separated by a space when they share a line. Consecutive closing delimiters can be adjacent. An invocation stays on one line when it fits RON's 100-column target. Otherwise, it wraps between complete arguments, with continuation lines indented two spaces from the invocation's starting column. This rule applies to all callees without recognizing function or special-form names. Valid references use .name and :name, invocations use parentheses (including ()), quotation forms use their short prefixes, and muted forms use \. Explicit tagged data that has no faithful shorthand, such as #local "two words", #const "", or #local ".x", retains explicit notation. A direct callee whose name begins with . or : also keeps the explicit #invoke notation. Formatting and parsing the result preserves the JSON representation. Comments on invocation elements remain attached to those elements.

DUST
[ (quote 1) (quasiquote [a b]) (unquote .x) (unquote-splicing .xs) ]
→
Canonical DUST
(quote 1)
(quasiquote [a b])
(unquote .x)
(unquote-splicing .xs)
→
RON
[#invoke [quote 1] #invoke [quasiquote [a b]]
 #invoke [unquote #local x] #invoke [unquote-splicing #local xs]]
DUST
{empty #local ""
 spaces #local "two words"
 non-string #local 42
 quoted-name #local ".x"
 colon-name #const ":x"
 non-list #invoke 42
 numeric-callee #invoke [1 2]
 spaced-callee #invoke ["two words" 1]
 var-callee #invoke [#local f 1]
 const-callee #invoke [#const f 1]
 dotted-callee #invoke [".f" 1]
 quoted-callee #invoke [#quote .f 1]
 empty-call #invoke []}
→
Canonical DUST
{colon-name #const ":x"
 const-callee (:f 1)
 dotted-callee #invoke [".f" 1]
 empty #local ""
 empty-call ()
 non-list #invoke 42
 non-string #local 42
 numeric-callee #invoke [1 2]
 quoted-callee (`.f 1)
 quoted-name #local ".x"
 spaced-callee #invoke [
   two words
   1]
 spaces #local "two words"
 var-callee (.f 1)}
→
RON
colon-name #const :x
const-callee #invoke [#const f 1]
dotted-callee #invoke [.f 1]
empty #local ""
empty-call #invoke []
non-list #invoke 42
non-string #local 42
numeric-callee #invoke [1 2]
quoted-callee #invoke [#quote #local f 1]
quoted-name #local .x
spaced-callee #invoke [
  two words
  1]
spaces #local "two words"
var-callee #invoke [#local f 1]
DUST
(f
  ; before the first argument
  1 ; first argument
  2)
→
RON
"#invoke" [f
           ; before the first argument
           1 ; first argument
           2]

DUST
(function-name "a string argument with several words" "a string argument with several words" "a string argument with several words" "a string argument with several words" )
→
Canonical DUST
(function-name
  "a string argument with several words"
  "a string argument with several words"
  "a string argument with several words"
  "a string argument with several words")
→
RON
#invoke [
  function-name
  a string argument with several words
  a string argument with several words
  a string argument with several words
  a string argument with several words]
Lists use square brackets. Strings use quotes or fences when a bare spelling would change the parsed form, including strings that begin with . or :.

DUST
["foo(bar)" "(bar)" "abc)" ".name" ":name"]
→
Canonical DUST
"foo(bar)"
"(bar)"
"abc)"
".name"
":name"
→
RON
[foo(bar) (bar) abc) .name :name]

These rules apply to DUST source, not str, which formats collections through the canonical RON formatter.

1.3. Values and Evaluation ↑

DUST is an expression-oriented programming language, thus every language construct evaluates to a value. We call these language constructs forms. Forms beget values by evaluation. At the bottom lie the self-evaluating values. These are the values that evaluate to themselves.

null is the value that represents the absence of a value. null is falsey.

true is the value that represents truth. true is truthy.

false is the value that represents falsehood. false is falsey.

Strings are RON strings. Strings are interned.

Numbers are RON numbers: integers are signed 64-bit and floats are IEEE 754 doubles. A literal outside those ranges, such as 1e400, is a parse error.

Numbers never convert implicitly. Arithmetic and comparison operands must be all integers or all floats, so (+ 1 2.5) is #err <mismatch type>; write (+ 1.0 2.5) or convert with float and int. Division is the one operation whose result kind differs from its operands: / always returns a float.

Operations check their results. Integer overflow is #err <integer overflow>, a float result that would be infinite or NaN is #err <float overflow>, and division by zero is #err <division integer zero> or #err <division float zero> by operand kind. NaN and infinity therefore never exist as numbers: a value can only hold a number that JSON can spell.

DUST does not have keywords because RON does not have keywords. But importantly: keywords are not necessary in a language like RON and DUST where strings are interned and supported first-class (no quotes)

1.3.1. References ↑

However DUST does need references. A reference is a name that refers to something else; in other languages you might know it as an identifier. DUST has two kinds, and the prefix says where the name is looked up:

Source Tagged form JSON Looks up
.name #local name {"#local": "name"} Local bindings: let, fn parameters, a function's own name, for, and top-level local
:name #const name {"#const": "name"} Constants: library entries and bindings supplied by the host

References follow the source notation rules. A quoted string whose contents begin with . or : is not a reference. Map keys are always strings, including names beginning with . or :. Parsing constructs a reference without resolving its name. Binding forms use string names rather than references.

A local reference resolves to the nearest enclosing local binding, whether its value is a function or ordinary data. With no enclosing binding, it is the top-level local of that name, read each time the reference runs. It never reaches a constant: .math/pi is unbound unless a local binding has that name. An unbound local is an error when evaluation reaches it, so (if true 1 .missing) returns 1.

A constant reference never reaches a local binding: (let [x 1] :x) is unbound. Library entries are fixed: a host binding with the same name as a library entry is rejected, and every evaluation with that binding produces #err <conflict name>, whether or not the program refers to the name. A host that needs its own value supplies it under another namespace. Every constant in a program is resolved once, before evaluation starts. If any of them is unbound, the program evaluates to that #err <name unbound>, even when the reference is in a branch that would not run. Constants inside quoted or muted forms are not evaluated, so they are not resolved.

DUST
(let [name Ada] .name)
=
DUST
.math/pi
=
DUST Result
#err <name unbound>
DUST
(if true 1 .missing)
=
DUST
:name
→
Evaluation cases
#cases [{bindings {name Ada} output Ada}]
DUST
(let [x 1] :x)
=
DUST Result
#err <name unbound>
DUST
(if false :missing 1)
=
DUST Result
#err <name unbound>
DUST
`[:missing .missing]
=
DUST Result
:missing
.missing
DUST
(+ 1 \:missing 2)
=
DUST
:math/pi
→
Evaluation cases
#cases [{bindings {math/pi 3} output #err <conflict name>}]
DUST
(+ 1 2)
→
Evaluation cases
#cases [{bindings {+ 0} output #err <conflict name>}]

A pointer has the type name pointer and holds a sequence of Key string segments and nonnegative Index integer segments. RFC 6901 text is used when constructing, encoding, or printing it. Decoding splits at /, replaces ~1 with / and ~0 with ~, and treats decimal digits with no leading zeroes as an index, saturating at the largest signed 64-bit integer. Encoding writes indices in decimal and escapes ~ before / in keys. After printing and reading again, Key "0" becomes Index 0. The two select the same map key, but only the index can select a list element.

Pointer equality compares segment kinds and values. list? is false for a pointer, and jsonptr? is true. str returns the RFC 6901 text, while format and the REPL print the canonical DUST form. Functions that accept arbitrary values also accept pointers, including equality, distinctness, and map values.

A direct invocation resolves the exact name stored in the first element of #invoke. It looks first in the local bindings and then in the constants, so both (increment 4) with a let-bound increment and (+ 1 2) work. ("+" 1 2) looks up a name containing the quote characters as well as the plus sign. (.f 1) and (:+ 1 2) are computed callees whose references follow the rules above. A missing binding produces an unbound-name error.

1.3.2. Evaluation ↑

Evaluation turns a form into a value. RON and JSON representations of the same source form have identical evaluation semantics. Runtime inputs and returned values are values, not source to evaluate again.

Known tagged elements are validated before evaluation. This includes known tags inside quotations, muted forms, and branches that would not be selected at runtime. Unknown tags remain generic tagged data. Evaluating one returns its tag and payload as data, without evaluating the payload or any nested invocation. For example, #custom (/ 1 0) preserves the invocation as data and does not divide by zero. Formatting and notation round trips preserve the normal tagged representation.

This opaque evaluation rule does not bypass import validation. Known tags nested inside an unknown payload are still validated eagerly; for example, #custom #local 42 has an invalid known reference payload. Today that is the error #err <invalid tag> when the form is evaluated; once readers use the cell decoders, such a source does not parse.

DUST
{#tag value}
→
Canonical DUST
#tag value
=
DUST Result
#tag value
DUST
#application {
  symbol .missing
  invocation (unknown-function .missing)
  unquote ,.missing
  splice ,@.missing
  nested #another (unknown-function (/ 1 0))
}
→
Canonical DUST
#application {invocation (unknown-function .missing)
              nested #another (unknown-function (/ 1 0))
              splice ,@.missing
              symbol .missing
              unquote ,.missing}
=
DUST Result
#application {invocation (unknown-function .missing)
              nested #another (unknown-function (/ 1 0))
              splice ,@.missing
              symbol .missing
              unquote ,.missing}
DUST
#application {known #local 42}
=
DUST Result
#err <invalid tag>

The self-evaluating forms already represent their value, so evaluating them results in themselves. Other forms describe how to obtain a value, for example by looking up a name to grab what it represents or invoking a function.

An invocation is encoded as a #invoke tagged element containing the callee followed by its argument forms.

For an ordinary function call, a string in the first element of #invoke names the function to resolve. Any other form in that position is evaluated once to obtain the function. Direct invocation names resolve to the nearest local binding, then to a constant. References resolve as described in References. The result must be a function.

A computed callee that returns a string is an error, even if that string names a function. There is no second name lookup.

DUST excludes muted argument forms, then evaluates the active argument forms from left to right and passes their resulting values to the function. The function's return value becomes the value of the invocation.

Examples show DUST source beside its expected result.

DUST
(+ 1 2)
=
DUST
(:+ 1 2)
=
DUST
("+" 1 2)
→
Evaluation cases
#cases [{bindings {""\"+""" (fn [a b] (+ .a .b))} output 3}]
DUST
((if true :+ :*) 2 3)
=
DUST
((if true "+" "*") 2 3)
→
Canonical DUST
((if true + *) 2 3)
=
DUST Result
#err <callee string>
DUST
(let [op (if true :+ :*)]
  (op 2 3))
→
Canonical DUST
(let [op (if true :+ :*)] (op 2 3))
=
DUST
(let [a-fn :+]
  (a-fn 1 2))
→
Canonical DUST
(let [a-fn :+] (a-fn 1 2))
=

(+ 1 (* 2 2)) can be thought of as being evaluated in two steps:

  1. Evaluate (* 2 2) to obtain 4.
  2. Substitute 4 for the multiplication, giving (+ 1 4), which evaluates to 5.

Hence:

DUST
(+ 1 (* 2 2))
=

Square brackets construct lists and braces construct maps. List construction excludes muted elements and evaluates the active elements from left to right. Map construction evaluates each member value2; the order in which member values are evaluated is not defined. Canonical key order is a formatting rule, not an evaluation order. A muted form cannot supply a map member value.

DUST
[+ 1 2]
→=
DUST
[(+ 1 2) 4]
→
Canonical DUST
(+ 1 2)
4
=
DUST
{total (+ 1 2)}
=
DUST Result
{total 3}

Tags parse into ordinary boolean map members before evaluation. Their names are string keys, not references, and their boolean values evaluate to themselves. Tags introduce no new evaluation behavior.

An evaluation error is a value. It can be passed to a function, bound by let, or stored in a list or map. Evaluation of other arguments and elements can continue. A program can inspect an error with error?.

An error is a #err tagged element. Its payload may be a string or a detailed map. A map can contain a RON tag group that classifies the error and members that describe the failure:

#err {<division integer zero>
      column 1
      line 1
      message "division by zero"}

Conformance expectations write only the tags, as #err <division integer zero>; messages and positions are diagnostics, not part of the contract. An error value retains its tags when passed through a program. Resource and allocation failures remain runtime failures that can stop evaluation.

Special forms control evaluation rather than following the ordinary function-call rule. For example, if evaluates only the selected branch.

Macros transform forms before evaluation, while quotation allows a form to be used as data without evaluating it.

1.3.2.1. Error tags ↑

Tags are a set, not a path: each tag is one word naming one facet of the failure, so an error can be looked up by any predicate it carries. overflow matches both integer and float overflow, integer matches every failure of integer arithmetic, mismatch matches type and arity mismatches, and mute matches every misuse of a muted form. Language errors use the tags below; Stardust errors carry stardust, and applications should add a tag naming themselves.

Tags Raised when Example
<mismatch type> An operand has the wrong type, including mixed integer and float arithmetic (+ 1 2.5)
<integer overflow> An integer, duration, or timestamp result leaves the 64-bit range (+ 9223372036854775807 1)
<float overflow> A float result would be infinite or NaN (* 1e308 10.0)
<division integer zero> An integer or duration divisor is zero (/ 1 0)
<division float zero> A float divisor is zero (/ 1.0 0.0)
<invalid value> An argument has the right type but an unusable value (time/duration forever)
<invalid tag> A known tag has a malformed payload #utc yesterday
<name unbound> A reference or callee has no visible binding .missing
<conflict name> A host binding has the name of a library entry binding math/pi
<arity mismatch> A function receives a number of arguments it does not accept ((fn [x] x))
<argument missing> A variadic operator receives no arguments where one is required (/)
<list missing> A function that takes lists receives none (map :strings/case)
<expected function> A callee evaluates to a non-function (map upper [ada])
<callee string> A computed callee evaluates to a string ((str "a") 1)
<fn missing name rest> & is not followed by a rest parameter name (fn [&] 1)
<arity fn multiple variadic> More than one arity is variadic (fn ([& a] 1) ([x & b] 2))
<arity duplicate fixed fn> Two arities take the same fixed count (fn ([x] 1) ([y] 2))
<arity exceeds fixed fn variadic> A fixed arity exceeds the variadic arity's minimum (fn ([x y] 1) ([& r] 2))
<mute root value> A muted form supplies the root value \1
<map mute value> A muted form supplies a map member value {a \1}
<callee mute> A muted form supplies a callee (\f 1)
<outside quasiquote unquote> Unquote appears outside quasiquote ,.x
<outside quasiquote splicing unquote> Unquote-splicing appears outside quasiquote ,@.x
<invalid quotation> Quasiquote splices into something other than a list #quasiquote {a #unquote-splicing [1]}
<algorithm hash unknown> hash/digest receives an unsupported algorithm (hash/digest md5 1)
<function unrepresentable> A host asks for a function value without its source form  
<invalid program> The engine rejects a program before evaluation  
<configuration invalid> The host configured the engine incorrectly  
<limit resource> Parsing or evaluation exceeds a CPU, memory, node, or depth limit 257 nested lists
<limit internal KIND> A valid program is outside an internal limit of the implementation 256 nested lists in Epsilon
<allocation failure> The host cannot allocate memory  
<failure runtime> An internal engine failure  

1.3.3. Collection Paths ↑

Collection paths select values in maps and lists. A string is always one literal map key, including a string that starts with /. A nonnegative integer is one index. A list contains only string keys and nonnegative integer indices; an empty list selects the root. A pointer from /..., #jsonptr, or jsonptr supplies its segments directly.

Segment Map List Null or missing entry
Key s Member s #err <mismatch type> Missing
Index n Member with decimal key n Zero-based element n Missing

An out-of-range list index is missing. A missing read returns the default or null, and contains? returns false. A write creates intermediate maps; an index in a new map uses its decimal key. A list write at its length appends. Other container types give #err <mismatch type>. A negative index gives #err <invalid value>. A pointer inside a list path is a type error.

A whole embedded segment becomes one segment based on its value:

Embedded value Result
String with valid index spelling Index
Other string Key, without further RFC 6901 decoding
Nonnegative integer Index
Negative integer #err <invalid value>
Null, boolean, float, list, map, pointer, or function #err <mismatch type>
Error The same error

A mixed segment always becomes a key, even if its resulting text is digits. Its literal parts and embedded strings are concatenated directly; null becomes null, and other values use their canonical DUST text.

1.4. Scope & Binding ↑

DUST is a lexically scoped language. What one might call variables in another language are called bindings in DUST. A scope is the region of code where a binding is valid. A reference with no visible binding produces an unbound-name error.

Libraries are loaded under a namespace, and a library file names the members it provides with export. What a library path resolves to is defined by each runtime: the Epsilon REPL resolves !load lib/math to the file lib/math.dust, relative to its working directory, while Stardust resolves paths within its database.

To create local bindings you need to introduce a new scope, and in DUST, like many lisps, the main way to do that is with the let special form.

1.5. Identity & Equality ↑

Equality asks whether two values contain the same data, regardless of how they were constructed or where they came from. Whereas identity asks whether references point to the same value.

Equality in DUST is by value, except for functions which compare by identity.

Equal values have equal hashes, because a hash covers a value's canonical bytes, which keep the integer/float distinction and drop comments. The converse fails only for functions: two closures with the same source hash alike but are not equal, since each is its own identity.

Since functions, constructs that do not exist in JSON or RON, can close over state it only makes sense for them compare by identity.

DUST provides = to test equality and comparison/identical? to test identity.

1.6. Truth and Control Flow ↑

Control-flow constructs in DUST are also expressions (reminder: everything is an expression), so they evaluate to values. DUST provides the following special forms and macros to enable branching and repetition. They decide which forms get evaluated and how often.

Every condition must be interpreted as either logically true or false. We must categorize each value as either logically true or false. DUST defines truth and falsehood so:

  • `null`, `false`, and #err values are falsey
  • everything else is truthy

An error used as a test therefore selects the false branch: (if ( 1 null) a b)= is b, since ( 1 null)= is an #err value.

We say that they are false/y/ or truth/y/ because they are logically true or false. To say they are true or false would imply they equal true or false, which obviously makes no sense.

DUST provides true? and false? to answer if some value is true or false. And boolean to coerce a value to true or false, and not to negate a value's truthiness.

1.6.1. Branching ↑

DUST provides one primitive special form for branching, if. The macros when, when-not, and cond build on it to express common branching patterns.

1.6.2. Looping ↑

Use for to iterate over collections.

1.7. Special forms and macros ↑

Special forms and macros control evaluation or introduce bindings. Ordinary functions are documented in the Function Index.

1.7.1. Function construction ↑

1.7.1.1. fn ↑

Special Form

(fn name? doc-or-meta? [params*] body*) → function
(fn name? doc-or-meta? ([params*] body*)+) → function

Compiles a function literal into a closure.

The optional name is local to the function and supports self-reference from every arity body. Functions can be selected, bound, passed, returned, and called. A function's canonical form is its fn source, so printing and hashing a closure use that form; values it captures are not part of it. A library function's canonical form is its constant reference, such as :strings/case. Recursion is supported. The optional function name and parameter names are strings, conventionally written bare. References to parameters in the body use ..

The optional doc-or-meta slot accepts either a string docstring or a map of metadata. A string docstring supplies the dust/doc entry in the function's metadata. If the first argument after fn is a string, it is always the function name, even when written with quotes. A string docstring can appear only after a name. An anonymous function can supply documentation in a metadata map before its parameter list or arity clauses.

Leading comments before the parameter list or first arity clause become the binary function definition's doc field. A comment on the fn constructor joins that block. Canonical formatting puts the block before the parameters or first clause; comments inside parameters and bodies stay on their values. See the constructor example and its canonical form.

1.7.1.1.1. Parameters and bodies ↑

A parameter list without & accepts exactly as many arguments as it has parameters. An empty parameter list accepts no arguments. A parameter list ending in & rest accepts additional arguments after its required parameters. The rest parameter receives those additional values as a nonempty list, in order, or null when there are none. The values are passed without copying or evaluating them again. The & marker can occur once at most and must be followed by exactly one parameter name. It is recognized by its string value, so "&" has the same meaning. A malformed parameter list is an error when compiling the function.

A required parameter may also be a map pattern. Each pattern key selects the same key from the argument map; its value names the local binding. A nested map pattern destructures that selected value recursively. Missing keys and null arguments supply null to the pattern. A non-map, non-null argument is a type error. Rest parameters remain string names. Other parameter pattern types are compilation errors. This map destructuring applies to fn parameters; it does not add destructuring to let or for.

For example, (fn [{quantity quantity unitPrice unitPrice}] (* .quantity .unitPrice)) binds the quantity and unitPrice members of its argument.

A body contains zero or more expressions, evaluated in order. Its final expression supplies the return value. An empty body returns null. An error value does not by itself stop later body expressions.

1.7.1.1.2. Multiple arities ↑

The single-arity form has one parameter list followed by its body expressions. The clause form has one or more parenthesized arity clauses, each with a parameter list followed by its body expressions. Both forms create one function. The optional name and documentation or metadata apply to the whole function. An arity clause parses as a #invoke tagged element containing its parameter list followed by its body forms. fn interprets this structure as a clause, not a call.

A function can have distinct fixed arities and at most one variadic clause. With a variadic clause, no fixed arity may exceed its required parameter count. Duplicate fixed arities, multiple variadic clauses, and fixed arities above that count are errors when compiling the function.

A call selects an exact fixed-arity match first. Otherwise, it selects the variadic clause if enough arguments were supplied for its required parameters. If neither matches, the call produces an arity error. Selection depends on argument count, not argument types or clause order. Only the selected body is evaluated, with parameter bindings local to that call. Ordinary calls and apply use the same selection rules.

Functions capture their lexical bindings, including bindings retained by escaping closures. Top-level functions can call each other through top-level locals. Tail-call guarantees remain to be defined.

1.7.1.1.3. Examples ↑
DUST
(let [increment (fn increment [x]
                   (+ 1 .x))]
  [(increment 0)
   (increment 1)
   (increment 41)])
→
Canonical DUST
(let [increment (fn increment [x] (+ 1 .x))] [(increment 0) (increment 1) (increment 41)])
=
DUST
(let [increment (fn increment "Return x plus one." [x]
                   (+ 1 .x))]
  [(increment 0)
   (increment 1)
   (increment 41)])
→
Canonical DUST
(let
  [increment (fn increment "Return x plus one." [x] (+ 1 .x))]
  [(increment 0) (increment 1) (increment 41)])
=
DUST
(let [increment (fn increment {dust/doc "Return x plus one."} [x]
                   (+ 1 .x))]
  [(increment 0)
   (increment 1)
   (increment 41)])
→
Canonical DUST
(let
  [increment (fn increment {dust/doc "Return x plus one."} [x] (+ 1 .x))]
  [(increment 0) (increment 1) (increment 41)])
=
DUST
(let [increment (fn {dust/doc "Return x plus one."} [x]
                   (+ 1 .x))]
  [(increment 0)
   (increment 1)
   (increment 41)])
→
Canonical DUST
(let
  [increment (fn {dust/doc "Return x plus one."} [x] (+ 1 .x))]
  [(increment 0) (increment 1) (increment 41)])
=
DUST
(let [increment (fn [x] (+ 1 .x))]
  (increment 4))
→
Canonical DUST
(let [increment (fn [x] (+ 1 .x))] (increment 4))
=
DUST
(let [greet (fn greet "Greet one or more people."
               ([name greeting & more]
                 (strings/join [.greeting ", " .name (strings/join .more "")] ""))
               ([] (greet world))
               ([name] (greet .name Hello))
               ([name greeting] (strings/join [.greeting ", " .name "!"] "")))]
  [(greet)
   (greet Ada)
   (greet Ada Hi)
   (greet Ada Hi " and " Bob "!")])
→
Canonical DUST
(let
  [greet
   (fn
     greet
     "Greet one or more people."
     ([name greeting & more]
       (strings/join
         [
           .greeting
           ", "
           .name
           (strings/join .more "")]
         ""))
     ([] (greet world))
     ([name] (greet .name Hello))
     ([name greeting]
       (strings/join
         [
           .greeting
           ", "
           .name
           !]
         "")))]
  [(greet) (greet Ada) (greet Ada Hi) (greet Ada Hi " and " Bob !)])
=
DUST Result
"Hello, world!"
"Hello, Ada!"
"Hi, Ada!"
"Hi, Ada and Bob!"

1.7.1.2. do ↑

Special Form

(do expr*) → value

Evaluates the expr forms in order and returns the last result.

With no expressions, returns null. An error value does not by itself stop later expressions. do introduces no bindings.

DUST
[(do)
 (do 7)
 (do 1 2 3)
 (if true (do ignored 42) (/ 1 0))]
→
Canonical DUST
(do)
(do 7)
(do 1 2 3)
(if true (do ignored 42) (/ 1 0))
=
DUST Result
null
7
3
42

1.7.2. let ↑

Special Form

(let [bindings*] exprs*)

Introduces local bindings and evaluates a body in their scope.

The binding list contains zero or more pairs of names and value expressions. Names are strings, conventionally written bare. A reference such as .price is not a binding name.

DUST
(let [price (* 5 2)
      quantity 3]
  (* .price .quantity))
→
Canonical DUST
(let [price (* 5 2) quantity 3] (* .price .quantity))
=

The value expressions are evaluated once and in order. Each binding becomes available to use in later expressions.

DUST
(let [price 10
      quantity 3
      total (* .price .quantity)]
  .total)
→
Canonical DUST
(let [price 10 quantity 3 total (* .price .quantity)] .total)
=

Bindings are in scope throughout the body of their let and within nested expressions. Inner scopes can introduce the same names, shadowing the outer bindings.

DUST
(let [price 10]
  [(let [price (+ .price 2)]
     .price)
   .price])
→
Canonical DUST
(let [price 10] [(let [price (+ .price 2)] .price) .price])
=

A function closes over the bindings it references from its surrounding lexical scope. The resulting closure has those bindings available when the function is called, even after execution has left that scope (indefinite extent).

DUST
(let [name outer
      read-name (fn [] .name)]
  (let [name inner]
    (read-name)))
→
Canonical DUST
(let [name outer read-name (fn [] .name)] (let [name inner] (read-name)))
=
DUST
(let [read-name (let [name outer]
                  (fn [] .name))]
  (read-name))
→
Canonical DUST
(let [read-name (let [name outer] (fn [] .name))] (read-name))
=

Body expressions inside the let are evaluated in order.

The let special form is an expression and evaluates to the value of the final expression in the body of the let, unless the body is empty in which case it evaluates to null.

DUST
(let [] 1 2)
=
DUST
(let [x 1])
=

Additional forms that introduce bindings are: fn, for, and local.

1.7.3. local ↑

Special Form

(local name value?) → local reference

Defines a top-level local: a binding that the later top-level forms of the same file, or of the same REPL session, read as .name.

local is a top-level form. Inside any other form it is #err <invalid program>. Each top-level form is evaluated on its own, so a form sees the locals that earlier forms define. (local name value) returns the reference .name.

DUST
(local a 10)
(+ .a 1)
=
DUST
(do (local a 1))
=
DUST Result
#err <invalid program>
DUST
(local a 1 2)
=
DUST Result
#err <arity mismatch>

A later local with the same name gives the same binding a new value. A reference to a top-level local is read each time it runs, so a function made earlier sees the new value, and a function can call a function that is defined after it, once that function exists.

DUST
(local a 10)
(local f (fn [] .a))
(local a 11)
(.f)
=
DUST Result
.a
.f
.a
11
DUST
(local h (fn [] (k)))
(local k (fn [] 42))
(h)
=
DUST Result
.h
.k
42

(local name) declares a local without a value, and leaves the value of a local that has one. A declared local is #err <name unbound> until a later local gives it a value. A declaration lets two top-level functions call each other, as in Clojure's declare.

DUST
(local g)
.g
=
DUST Result
.g
#err <name unbound>
DUST
(local odd?)
(local even? (fn [n] (if (= .n 0) true (odd? (- .n 1)))))
(local odd? (fn [n] (if (= .n 0) false (even? (- .n 1)))))
(even? 10)
=
DUST Result
.odd?
.even?
.odd?
true

When the value is an #err value, local returns that value and the local is declared without a value.

DUST
(local bad (/ 1 0))
.bad
=
DUST Result
#err <division integer zero>
#err <name unbound>

A let, fn, or for binding with the same name shadows a top-level local. A constant reference never reaches a top-level local. The name of a library entry is #err <conflict name>, because a direct call of that name always calls the library entry.

DUST
(local a 10)
(let [a 1] [.a (+ .a 1)])
=
DUST Result
.a
[1 2]
DUST
(local a 10)
:a
=
DUST Result
.a
#err <name unbound>
DUST
(local map 1)
=
DUST Result
#err <conflict name>

1.7.4. export ↑

Special Form

(export name*) → list

Names the top-level locals that a library file provides. When a runtime loads the file under a namespace, as !load lib/shapes s does in the Epsilon REPL, each named local becomes the constant :s/name, with the value the local has when the file has run. The other locals of the file stay private, and the file's functions read them, not the locals of the code that calls them.

; lib/shapes.dust
(local area (fn [w h] (* .w .h)))
(local square (fn [s] (area .s .s)))
(local unit 1)
(export square unit)

After !load lib/shapes s, (s/square 3) returns 9 and :s/unit returns 1, while :s/area is #err <name unbound>.

export is a top-level form, and a file can have more than one. A file with no export provides nothing. A load fails when export names a local that does not exist or has no value, or when a top-level form returns an #err value. Evaluated directly, export returns the list of the named references and has no other effect.

DUST
(local a 1)
(export a)
=
DUST Result
.a
[.a]

1.7.5. Control Flow ↑

1.7.5.1. and ↑

Special Form

(and forms*) → value

Evaluates forms once each, from left to right, and returns the first falsey value without evaluating later forms; an #err value is falsey, so and returns it. If every form is truthy, returns the last value. With no forms, returns true.

Only false, null, and #err values are falsey; zero and the empty string are truthy. The result is an operand value rather than a conversion to boolean.

DUST
(and)
(and 0 "" x)
(and false (/ 1 0))
(and null (/ 1 0))
(let [m {n 0}] [(and (do (patch .m {n (+ (get .m n) 1)}) x) y) (get .m n)])
=
DUST Result
true
x
false
null
[y 1]

1.7.5.2. or ↑

Special Form

(or forms*) → value

Evaluates forms once each, from left to right, and returns the first truthy value without evaluating later forms. If every form is falsey, returns the last value. With no forms, returns null.

Only false, null, and #err values are falsey; zero and the empty string are truthy. The result is an operand value rather than a conversion to boolean.

DUST
(or)
(or null false x)
(or 0 (/ 1 0))
(or "" (/ 1 0))
(let [m {n 0}] [(or (do (patch .m {n (+ (get .m n) 1)}) null) y) (get .m n)])
=
DUST Result
null
x
0
""
[y 1]

1.7.5.3. if ↑

Special Form

(if test then)
(if test then else)

Selects a branch according to the truthiness of a test.

The test is evaluated once. The resulting value's truthiness is used to choose a branch. A truthy result selects then, and a falsey result selects else. Only the selected branch is evaluated, and its value becomes the value of the whole if expression. If else is omitted, a falsey test evaluates to null.

Each branch contains one expression. if is a special form because it controls which arguments are evaluated, rather than evaluating all of them before making the choice.

DUST
(if true accepted rejected)
=
DUST Result
accepted
DUST
(if false accepted rejected)
=
DUST Result
rejected
DUST
(if false accepted)
=
DUST
(if true 42 (/ 1 0))
=

1.7.5.4. when ↑

Macro

(when test body...)

Runs a body when its test is truthy.

It evaluates the test once, then evaluates the body expressions in order and returns the last result. If the test is falsey, the body is not evaluated and the result is null. An empty body also returns null.

Unlike if, when has no alternative branch and can contain several body expressions.

DUST
(when true ready)
=
DUST
(when false ready)
=
DUST
(when true 1 2)
=
DUST
(when false (/ 1 0))
=
DUST
(when true)
=

1.7.5.5. when-not ↑

Macro

(when-not test body...)

Runs a body when its test is falsey.

Like when, it evaluates the test once, evaluates the selected body in order, and returns the last result. A truthy test skips the body and returns null, and an empty body returns null regardless of the test's value.

DUST
(when-not false ready)
=
DUST
(when-not null 1 2)
=
DUST
(when-not true ready)
=

1.7.5.6. cond ↑

Macro

(cond test result ...)

Chooses among pairs of tests and result expressions.

It evaluates tests once each, in order, until one is truthy. It then evaluates and returns that test's result expression, without evaluating any later tests or results. A selected result of false or null is still the result, not a reason to continue to another pair.

If no test is truthy, cond returns null. With no pairs, it also returns null. Every test must have a result expression, so an unmatched final test is an error during macro expansion. A final pair with true as its test supplies a default result. This is an ordinary truthy test, not a separate default-branch syntax.

DUST
(cond false first true second)
=
DUST
(cond true false true fallback)
=
DUST
(cond false first)
=
DUST
(cond)
=

1.7.5.7. for ↑

Macro

(for [name collection ...] body)

Evaluates a body for each combination of collection values and returns a list of the results.

Evaluation is eager: all iterations complete before for returns. Binding names are strings, conventionally written bare, rather than references. The bindings are local to the for.

DUST
(for [x [1 2 3]]
  (* .x .x))
→
Canonical DUST
(for [x [1 2 3]] (* .x .x))
=

Multiple binding pairs introduce nested iteration, with the leftmost binding as the outermost loop. Lists are traversed in order. The body evaluates once per combination, contributing one value to the result list.

DUST
(for [x [1 2]
      y [10 20]]
  (+ .x .y))
→
Canonical DUST
(for [x [1 2] y [10 20]] (+ .x .y))
=
DUST Result
11
21
12
22

Each collection expression is evaluated once each time its loop is entered. Later collection expressions can reference earlier bindings. An empty collection contributes no combinations.

DUST
(for [xs [[1 2] [3]]
      x .xs]
  .x)
→
Canonical DUST
(for [xs [[1 2] [3]] x .xs] .x)
=
DUST
(for [x []]
  .x)
→
Canonical DUST
(for [x []] .x)
=

The body contains one expression. A let in the body can introduce additional bindings for calculated values. If the body produces a list, that list is one element of the result.

DUST
(for [x [1 2]]
  [.x])
→
Canonical DUST
(for [x [1 2]] [.x])
=
DUST Result
[[1] [2]]

1.7.6. Quotation ↑

1.7.6.1. quote ↑

Special Form

#quote form

Returns the enclosed form as data without evaluating it. References, invocations, and collection contents retain their form representation. See Quotation forms for prefix syntax and encoding.

1.7.6.2. quasiquote ↑

Macro

#quasiquote form

Constructs data while evaluating unquotes at the matching nesting depth. List-element splices insert the elements of their evaluated list. See Quotation forms for prefix syntax and encoding.

1.7.6.3. unquote ↑

Macro

#unquote form

Evaluates the enclosed form within its matching quasiquote and substitutes the resulting value. Outside quasiquote, this is an error. See Quotation forms for prefix syntax and encoding.

1.7.6.4. unquote-splicing ↑

Macro

#unquote-splicing form

Evaluates the enclosed list within its matching quasiquote and inserts its elements into the surrounding list. A non-list result or a splice outside a list-element position is an error. See Quotation forms for prefix syntax and encoding.

Footnotes:

1

For Lispers, DUST does not execute code during reads. There are no reader macros. Hence parser, not reader.

2

Because DUST forms have a JSON representation, keys are always (always!) strings.