1. Command line

Command Does
epsilon convert INPUT OUTPUT [--from FORMAT] [--to FORMAT] [--no-sort] Converts between formats or formats a document
epsilon convert --from FILES --to FILES [--no-sort] Converts every file a path or glob names
epsilon fmt [PATH...] [--write] [--check] [--from FORMAT \vert FILES] [--include GLOB] [--exclude GLOB] Formats DUST and RON files, keeping their key order
epsilon eval FILE Evaluates FILE form by form and writes each result as canonical DUST
epsilon repl [FILE...] Starts the REPL, loading each FILE as a namespace first
epsilon nrepl-server [options] Serves editor clients over TCP

convert supports .json, .edn, .tron, .ron, and .dust. The input and output extensions select the formats. Using the same extension for both files writes a normalized representation. The output file is written only after the input has been parsed and converted successfully.

Use - for standard input or standard output. Specify standard input's format with --from FORMAT and standard output's format with --to FORMAT. The format names are json, edn, tron, ron, and dust. These options can also override a file extension. Use --comment-roundtrip to preserve DUST and RON comment placements in JSON's $comment strings and interpret those strings when converting back. Without this option JSON $comment strings remain literal.

epsilon convert input.dust output.json
epsilon convert input.dust - --to json
cat input.edn | epsilon convert - output.dust --from edn
cat input.dust | epsilon convert - - --from dust --to json

--from and --to also take files. --from is then a path or a glob, and --to a path, or a directory followed by *.EXT or **/*.EXT. Each matching file is converted to the path it maps to: the --to directory, then, with **, the file's directories below the --from directory, then its name with the --to extension. Formats come from the extensions. Missing directories are created, an output that would not change is not rewritten, and two inputs that map to one output are an error before anything is written. A single --from file with a --to format name goes to standard output.

epsilon convert --from order.ron --to order.dust
epsilon convert --from 'data/**/*.json' --to 'out/**/*.dust'   # data/x/b.json -> out/x/b.dust
epsilon convert --from 'data/*.json' --to 'out/*.ron'          # top level of data only

In a glob, *, ?, and [...] match within one name and a ** name matches any number of names, none included. ** does not enter hidden directories, and hidden files match only a name pattern that starts with ..

Run epsilon convert -h for command help and examples.

DUST and RON output sorts map keys and the tags in a tag group, which gives the canonical form. --no-sort keeps both in source order instead. Either way a map's tag group comes first. A source that does not parse gives a #err diagnostic with its line and column on standard error and exit status 1.

fmt is convert from each file to its own format, dust or ron, with --no-sort, and its output ends with a newline, as editors save files: DUST output already does, and fmt adds one to RON output, which convert writes without one. A PATH is a file, a directory, or - for standard input (with --from dust or --from ron); with no PATH and no --from files, it is the current directory. A directory is searched recursively for files that match an --include glob (default *.dust and *.ron) and no --exclude glob. Hidden files and directories, build, and vendor are always skipped, and an excluded directory is not entered. A glob without / matches a name at any depth; one with / matches the path below the directory. --from also takes files, as for convert.

One file prints its result. --write (-w) rewrites each file whose result differs: it writes a temporary file in the same directory, gives it the file's permissions, and renames it over the file, so the file is never half written. Through a symbolic link it rewrites the target and keeps the link. --check writes nothing; it names each file formatting would change on standard error. Several files need --write or --check. A file that does not parse gives its path and a #err diagnostic on standard error, and the other files are still formatted. The exit status is 1 when any file failed or would change, and 2 for an invalid combination of options.

epsilon fmt order.dust
epsilon fmt --write order.dust
epsilon fmt --check                                 # the current directory
epsilon fmt --write src --exclude fixtures
epsilon fmt --check . --include '*.dust'
epsilon fmt --write --from 'src/**/*.dust'
cat order.dust | epsilon fmt - --from dust

For example, with tags and keys out of order, and with a root list:

Input epsilon fmt epsilon convert ... --to dust
{b 1 a 2 <z x>} {<z x> / = b 1= / = a 2}= {<x z> / = a 2= / = b 1}=
[1 2 7] 1, 2, 7 on three lines the same

EDN lists and sets become arrays, while keywords, symbols, and characters become text. Map keys become strings. Numeric ratios and decimal values become floating point numbers; integers outside the supported range are rejected. EDN #inst values are represented by #utc, and #uuid values become strings. Other EDN tags retain their tag and value as DUST tagged elements. DUST tags are written as EDN tagged elements when their names can be read as EDN tags; otherwise they are represented as maps. These mappings do not preserve all original EDN types. Only the first EDN form is read; an empty input reads as nil.

TRON encodes JSON-compatible values as a binary document. Floating-point values retain their numeric kind, including integral floats such as 1.0. This can differ from encoders that normalize integral floats to integers. A #b64 tagged value with valid base64 is represented as binary data and decoded back to that tag.

task repl builds the engine and starts the REPL from the repository.

Each REPL and each nREPL session may use 1 GiB of runtime memory, because it keeps its top-level locals and namespaces between evaluations; epsilon eval may use 64 MiB. An evaluation that needs more fails, and the garbage it leaves is collected before the next one. !reset returns the REPL to a fresh runtime.

Table of Contents

1.1. nREPL server ↑

Start a server for an editor client:

epsilon nrepl-server
epsilon nrepl-server --port 7888
epsilon nrepl-server --bind 127.0.0.1 --port-file /tmp/epsilon.port

The default address is 127.0.0.1 and the default port is 0, which asks the operating system to choose a port. The server writes that port to .nrepl-port in the current directory, or to the path given by --port-file. On shutdown it removes the file only if it still contains this server's port. The protocol has no authentication; choose the bind address accordingly.

Messages use bencode. These examples show their contents as maps:

client -> {op "clone", id "1"}
server <- {id "1", new-session "s-1", status ["done"]}

client -> {op "eval", id "2", session "s-1", code "(+ 1 2)\n(+ 3 4)"}
server <- {id "2", session "s-1", value "3"}
server <- {id "2", session "s-1", value "7"}
server <- {id "2", session "s-1", status ["done"]}

load-file loads the DUST source in file into a session, as !load does for a file in the interactive REPL: the locals its export forms name become constants. The server does not read the disk, so an editor can send an unsaved buffer:

client -> {op "load-file", id "3", session "s-1", file "(local unit 7)\n(export unit)", file-path "lib/shapes.dust", file-name "shapes.dust"}
server <- {id "3", session "s-1", status ["done"]}
client -> {op "eval", id "4", session "s-1", code ":shapes/unit"}
server <- {id "4", session "s-1", value "7"}

The namespace is the file-name without its extension, or else the file-path without its directory and extension. Add ns "s" to the load request to use :s/unit as the name; a request without ns, file-name, or file-path is rejected. file-path also names the source in error responses. Reloading the same namespace replaces its bindings. lookup reports the name and type of a symbol from the session or the core libraries:

client -> {op "lookup", id "5", session "s-1", sym "shapes/unit"}
server <- {id "5", session "s-1", info {name "shapes/unit", type "integer"}, status ["done"]}

If the name is unknown, the response omits info.

describe maps each operation name to a descriptor. The descriptors are empty unless the request has verbose?, which adds each operation's doc and, for eval, its requires, optional, and returns fields:

client -> {op "describe", id "6"}
server <- {id "6", ops {clone {}, close {}, describe {}, eval {}, interrupt {}, load-file {}, lookup {}, stdin {}}, status ["done"]}

clone starts a fresh session; copying a session is unsupported. eval returns each top-level form's result separately, as one DUST form: a list keeps its brackets, so [1 2 (+ 3 4)] returns [1 2 7] and [] returns [], and map keys are in canonical order. A session keeps the top-level locals that its evaluations define. A returned #err is a DUST value: it arrives in value, with file, line, and column for its source. Parse, compile, and runtime failures arrive in ex. interrupt stops an evaluation at a checkpoint and leaves the session usable. Completed mutations are not rolled back. Run epsilon nrepl-server -h for the options and examples.

eval writes the result to standard output and exits with 0. A failure is written to standard error as a #err element and exits with 1:

$ echo '(/ 1 0)' > zero.dust
$ epsilon eval zero.dust
#err {<division integer zero>
      column 1
      line 1
      message "division by zero"}

The tag group classifies the failure; the other members are diagnostics. See error tags for every tag set.