PAKT — a typed data interchange format. Human-authorable. Streaming. Self-describing.
This document defines PAKT 0 (draft). The specification is not yet stable — breaking changes may occur before PAKT 1.
PAKT uses single-integer versioning. Each version is a complete specification. Parser libraries advertise which spec versions they support.
| Version | Status | Meaning |
|---|---|---|
| PAKT 0 | Draft | In active development; breaking changes expected |
| PAKT 0 | Accepted | Feature-complete; only clarifications and bug fixes |
| PAKT 1 | (future) | First stable release; no breaking changes within major version |
-
Performance is a feature. The format, grammar, and type system are designed so that a conforming parser can operate in a single streaming pass with minimal allocation. Spec rules must not require unbounded buffering or retroactive reinterpretation.
-
Type context flows with the data. Every value carries or inherits its type. The parser never guesses. This enables type-directed parsing without schema negotiation.
-
The decoder is lossless; interpretation is layered. A conforming decoder preserves all information present in the source — including duplicate statement names, duplicate map keys, and encounter order. Policy decisions such as rejecting duplicates, applying last-wins, or accumulating values belong to higher-level consumers, not the core decoder.
-
Presentation is an application concern. Human-readable formatting, event enrichment, and display transformations (such as CLI output or JSON projection) are not encoder or decoder responsibilities. The core event contract is minimal and machine-oriented.
-
The grammar is the event model. Each grammatical construct — assign, pack, struct, tuple, list, map, scalar — maps to a distinct event kind. Consumers should not need to inspect payload strings to determine structural context.
A PAKT unit is a sequence of statements at the top level. A statement is either an assign (a named, typed, single value) or a pack (a named, typed, open-ended sequence of values).
The unit root uses self-describing statements (name:type = value or name:type << values...) rather than positional values. This is the system boundary where data enters without a parent type context.
name:str = 'midwatch'
version:(int, int, int) = (1, 0, 0)
Packs deliver zero or more values of a collection type, terminated by end-of-unit or the start of the next statement:
events:[{ts:ts, level:str, msg:str}] <<
{ 2026-06-01T14:30:00Z, 'info', 'server started' }
{ 2026-06-01T14:31:00Z, 'warn', 'high latency' }
A PAKT unit is a sequence of Unicode code points encoded as UTF-8. No other encodings are permitted.
A UTF-8 BOM (U+FEFF) at the start of a unit is accepted and ignored. Implementations must not reject a unit that begins with a BOM, and must not emit a BOM when writing.
ALPHA = 'a'-'z' | 'A'-'Z'
DIGIT = '0'-'9'
IDENT = (ALPHA | '_') (ALPHA | DIGIT | '_' | '-')*
WS = ' ' | '\t'
NL = '\n' | '\r\n'
SEP = ',' | NL
COMMENT = '#' (any char except NL)* NL
Line comments begin with # and extend to end of line. May appear on their own line or after a value. No block comments.
DIGIT_SEP = DIGIT (DIGIT | '_')*
HEX_DIGIT = DIGIT | 'a'-'f' | 'A'-'F'
BIN_DIGIT = '0' | '1'
OCT_DIGIT = '0'-'7'
INT = ['-'] DIGIT_SEP
| ['-'] '0x' HEX_DIGIT (HEX_DIGIT | '_')*
| ['-'] '0b' BIN_DIGIT (BIN_DIGIT | '_')*
| ['-'] '0o' OCT_DIGIT (OCT_DIGIT | '_')*
DEC = ['-'] DIGIT_SEP? '.' DIGIT_SEP
FLOAT = ['-'] DIGIT_SEP? ('.' DIGIT_SEP)? ('e' | 'E') [+-]? DIGIT+
BOOL = 'true' | 'false'
NIL = 'nil'
DATE = DIGIT{4} '-' DIGIT{2} '-' DIGIT{2}
TZ = 'Z' | [+-] DIGIT{2} ':' DIGIT{2}
TS = DATE 'T' DIGIT{2} ':' DIGIT{2} ':' DIGIT{2} ('.' DIGIT+)? TZ
UUID = HEX_DIGIT{8} '-' HEX_DIGIT{4} '-' HEX_DIGIT{4} '-' HEX_DIGIT{4} '-' HEX_DIGIT{12}
BIN = 'x' "'" HEX_DIGIT* "'"
| 'b' "'" BASE64_CHAR* "'"
BASE64_CHAR = ALPHA | DIGIT | '+' | '/' | '='
ESCAPE = '\' ('\' | "'" | '"' | 'n' | 'r' | 't')
| '\u' HEX_DIGIT{4}
string_char = ESCAPE
| any code point except matching_quote, '\', NL, U+0000
raw_char = any code point except matching_quote, U+0000
ML_CHAR = ESCAPE
| any code point except U+0000, not forming the closing triple-quote
ML_BODY = ML_CHAR*
raw_ml_char = any code point except U+0000, not forming the closing triple-quote
STRING = "'" string_char* "'"
| '"' string_char* '"'
RAW_STR = 'r' "'" raw_char* "'"
| 'r' '"' raw_char* '"'
ML_STR = "'''" ML_BODY "'''"
| '"""' ML_BODY '"""'
ML_RAW = 'r' "'''" raw_ml_char* "'''"
| 'r' '"""' raw_ml_char* '"""'
ATOM = '|' IDENT
- Leading zeros on decimal
INTare permitted and ignored (01evaluates to1). _in numeric literals is a visual separator, ignored by the parser.ATOMvalues use a|prefix to distinguish them from identifiers. Atom set type declarations use bare names inside pipe delimiters (|dev, staging, prod|), while atom values use the prefix (|dev).binliterals use a prefix to indicate encoding:x'...'for hexadecimal,b'...'for base64.- Hex literals must contain an even number of hex digits (each pair is one byte); an odd count is a parse error. Whitespace within hex literals is not permitted.
- Base64 literals follow RFC 4648 standard encoding. Padding (
=) is required. Invalid base64 characters or incorrect padding are parse errors. - Strings are always quoted. There are no unquoted string values.
A raw string is prefixed with r and performs no escape processing. Backslashes are literal.
path:str = r'C:\Users\alice\Documents'
regex:str = r"^\d{3}-\d{4}$"
Raw strings may also be triple-quoted for multi-line content. Indentation stripping follows the same rules as regular multi-line strings (based on the first non-blank content line), but no escape sequences are processed:
template:str = r'''
Hello \n World
'''
# Result: "Hello \\n World" — the \n is two literal characters
The only character that cannot appear in a raw string is the unescaped closing quote (or triple-quote for multi-line). To include the closing delimiter, use the alternate quote style:
has_single:str = r"it's raw"
has_double:str = r'she said "hello"'
Null bytes (U+0000) are not permitted in raw strings.
A multi-line string is delimited by triple quotes (''' or """).
Opening: The opening ''' must be followed immediately by a newline. Content begins on the next line. The first newline after the opening delimiter is stripped.
Closing: The closing ''' must appear on its own line, preceded only by whitespace. The last newline before the closing delimiter is stripped.
Indentation stripping: The leading whitespace of the first non-blank content line defines the baseline indentation. That many characters of leading whitespace are removed from each content line (including the first). A non-blank content line with fewer leading whitespace characters than the baseline is a parse error. Blank content lines (containing only whitespace) are preserved as empty lines. This rule allows streaming parsers to determine the strip level after reading a single line.
Escapes: The same escape sequences as single-line strings are recognized inside multi-line strings.
# First content line has 4 leading spaces → strip 4 from each line
query:str = '''
SELECT id, name
FROM users
WHERE active = true
'''
# Result: "SELECT id, name\nFROM users\nWHERE active = true"
# First content line has 0 leading spaces → no stripping
raw:str = '''
line one
line two
'''
# Result: "line one\nline two"
Within a quoted string (not raw), the following escape sequences are recognized:
| Escape | Meaning |
|---|---|
\\ |
Backslash |
\' |
Single quote |
\" |
Double quote |
\n |
Newline (U+000A) |
\r |
Carriage return (U+000D) |
\t |
Tab (U+0009) |
\uXXXX |
Unicode BMP code point (4 hex digits) |
Any other \ followed by a character is a parse error. Null bytes (U+0000) are not permitted in strings, whether literal or escaped. Surrogate code points (U+D800–U+DFFF) are not valid in \u escapes — they are an encoding detail, not code points. To include supplementary-plane characters (e.g., emoji), use literal UTF-8.
HASH = '#'
ASSIGN = '='
COLON = ':' ; type annotation only
SEMI = ';' ; map key-value association
PACK = '<<'
COMMA = ','
PIPE = '|'
LBRACE = '{' RBRACE = '}'
LPAREN = '(' RPAREN = ')'
LBRACK = '[' RBRACK = ']'
LANGLE = '<' RANGLE = '>'
; Reserved tokens (see §4.5)
AT = '@'
BANG = '!'
STAR = '*'
DOLLAR = '$'
AMP = '&'
TILDE = '~'
BTICK = '`'
Every value must have a type. There is no default type and no type inference.
| Type | Literal | Example |
|---|---|---|
str |
Quoted string | 'hello' |
int |
Signed 64-bit integer | 42, -7, 0xFF, 0b1010, 0o77, 1_000 |
dec |
Exact decimal | 3.14, 1_000.50 |
float |
IEEE 754 binary64 | 6.022e23, 1.5E-10 |
bool |
Boolean keyword | true, false |
uuid |
UUID | 550e8400-e29b-41d4-a716-446655440000 |
date |
ISO date | 2026-06-01 |
ts |
ISO timestamp (tz required) | 2026-06-01T14:30:00Z |
bin |
Binary data | x'48656C6C6F', b'SGVsbG8=' |
Numeric precision:
int: Signed 64-bit integer. Range: −9,223,372,036,854,775,808 to 9,223,372,036,854,775,807. Values outside this range are a parse error.float: IEEE 754 binary64 (double precision). Implementations must parse and round per IEEE 754.dec: Arbitrary-precision decimal in the text representation. Implementations must support at least 28 significant digits. An implementation may reject values exceeding its supported precision with a clear error.bin: Raw byte sequence. Hex form (x'...') and base64 form (b'...') are semantically equivalent — both produce the same bytes. Implementations represent this as a byte array/slice.
true, false, and nil are reserved keywords, not atoms.
Any type may be made nullable by appending ?:
nullable_type = type '?'
A nullable type accepts all values of the base type plus nil. A non-nullable type receiving nil is a parse error.
An atom set constrains a value to one of a fixed set of bareword identifiers:
atom_set = PIPE IDENT (COMMA IDENT)* PIPE
Example: |dev, staging, prod|
true, false, and nil are reserved keywords and cannot be used as atoms.
Atom values in data position use the | prefix: |dev, |staging, |prod.
| Kind | Type syntax | Delimiter | Keys | Values |
|---|---|---|---|---|
| Struct | {field:type, ...} |
{ } |
Atom (static) | Heterogeneous |
| Tuple | (type, ...) |
( ) |
None (positional) | Heterogeneous |
| List | [type] |
[ ] |
None (ordered) | Homogeneous |
| Map | <keytype ; valtype> |
< > |
Any typed value | Homogeneous |
Five unique delimiter pairs — no overloads:
First token after = |
Kind |
|---|---|
{ |
Struct |
( |
Tuple |
[ |
List |
< |
Map |
| anything else | Scalar or atom |
Empty lists ([]), empty maps (<>), empty structs ({}), and empty tuples (()) are valid. Empty atom sets are parse errors — an atom set requires at least one member.
The following tokens are reserved for future use. They must not appear in units outside of string literals. A conforming parser encountering a reserved token in an unexpected position should report a syntax error.
| Token | Possible future use |
|---|---|
@ |
Constraints (@len, @range, @pattern) |
! |
Assertions or negation |
* |
Wildcards or glob patterns |
$ |
Variable references or interpolation |
& |
Type aliases (see §4.6) |
~ |
Approximate matching or home paths |
` |
Alternate string delimiters or template literals |
Status: Design sketch only. The
&token is reserved for this purpose.
Type aliases would allow naming a type once and referencing it by name in type positions:
&entry = {path:str, size:int, is_dir:bool, hash:bin?}
&level = |info, warn, error|
entries:[&entry] << ...
events:[{ts:ts, level:&level, msg:str}] << ...
Planned semantics:
&name = typedefines a type alias. It is not a statement — it emits no events and does not participate in root duplicate handling.&namein type position is a structural substitution — the alias expands to the underlying type. There is no nominal typing.- Must appear before use. This is the streaming-compatible ordering rule. Forward references are a parse error. In practice, aliases go at the top of the unit.
&sigil required in both definition and reference. This keeps alias names and statement names in separate namespaces — a unit can have both&entry(type alias) andentry(statement name) without conflict.- Aliases can reference earlier aliases.
&log = {ts:ts, level:&level}is valid if&levelis already defined.
unit = statement*
statement = assign | pack
assign = IDENT type_annot ASSIGN value
pack = IDENT type_annot PACK pack_body
A pack delivers zero or more bare values of the annotated collection type. The type must be a list type or map type.
pack_body = list_pack_body | map_pack_body
list_pack_body = (value (SEP value)* SEP?)?
map_pack_body = (map_entry (SEP map_entry)* SEP?)?
For list packs, each value conforms to the list's element type. For map packs, each entry is key ; value conforming to the map's key and value types.
Termination: A pack ends at end-of-unit (EOF or NUL terminator per §10.1) or when the parser encounters the start of the next top-level statement (IDENT COLON). This is LL(0)-decidable: atom values begin with |, booleans and nil are reserved keywords that cannot be statement names, and all other value forms begin with non-identifier characters (digits, quotes, delimiters). Therefore a bare identifier at pack level always begins a new statement.
Duplicate keys: Repeated map entries are preserved in encounter order. Interpreting duplicate keys is an application/domain concern (see §6).
Root duplicates: Pack names participate in root duplicate handling — see §6.
type_annot = COLON type
type = (scalar_type | atom_set | struct_type | tuple_type | list_type | map_type) '?'?
scalar_type = 'str' | 'int' | 'dec' | 'float' | 'bool' | 'uuid' | 'date' | 'ts' | 'bin'
atom_set = PIPE IDENT (COMMA IDENT)* PIPE
struct_type = LBRACE (struct_field_decl (COMMA struct_field_decl)*)? RBRACE
struct_field_decl = IDENT COLON type
tuple_type = LPAREN (type (COMMA type)*)? RPAREN
list_type = LBRACK type RBRACK
map_type = LANGLE type SEMI type RANGLE
value = scalar | NIL | atom_val | struct_val | tuple_val | list_val | map_val
scalar = STRING | RAW_STR | ML_STR | ML_RAW | INT | DEC | FLOAT | BOOL | UUID | DATE | TS | BIN
atom_val = ATOM
nil is valid only when the type is nullable (type?).
Struct values contain positional values matched left-to-right against the fields declared in the type annotation. The type annotation is required.
struct_val = LBRACE struct_members RBRACE
struct_members = (value (SEP value)* SEP?)?
A struct value must contain exactly as many values as the type declares fields. Fewer or more values than declared is a parse error. Arity is validated during parsing.
Future consideration: Self-describing struct members (
name:type = value) may be added in a future version.
Tuple values contain positional values matched left-to-right against the types declared in the type annotation. The type annotation is required.
tuple_val = LPAREN tuple_members RPAREN
tuple_members = (value (SEP value)* SEP?)?
A tuple value must contain exactly as many values as the type declares elements. Fewer or more values than declared is a parse error. Arity is validated during parsing.
Future consideration: Self-describing tuple members (
:type value) may be added in a future version.
list_val = LBRACK list_members RBRACK
list_members = (value (SEP value)* SEP?)?
All elements must conform to the declared element type. An empty list ([]) is valid.
map_val = LANGLE map_entries RANGLE
map_entries = (map_entry (SEP map_entry)* SEP?)?
map_entry = value SEMI value
Keys conform to the declared key type. Values conform to the declared value type. Key-value pairs are separated by ;. An empty map (<>) is valid. Duplicate keys are preserved in encounter order; interpreting them is an application/domain concern (see §6).
Duplicate names at the unit root are not a format-level parse error and do not carry built-in replacement semantics.
A conforming decoder preserves repeated statements in encounter order. How duplicates are interpreted is an application/domain concern. Higher-level consumers or bindings may choose to reject duplicates, apply first-wins or last-wins semantics, accumulate all values, or preserve raw order for later processing, but they must document that behavior.
The reserved keywords true, false, and nil cannot be used as statement names.
Duplicate keys in either a map value (= <...>) or a map pack (<<) are not a format-level parse error and do not carry built-in replacement semantics in the format itself.
A conforming decoder preserves repeated map entries in encounter order. How duplicates are interpreted is an application/domain concern. Higher-level consumers or bindings may choose to reject duplicates, apply first-wins or last-wins semantics, accumulate all values, or preserve raw order for later processing, but they must document that behavior.
Struct field names are declared in the type, not in the value, so duplicates are caught at the type level.
- Whitespace around
=is optional:name:str = 'x'andname:str='x'are equivalent. - Whitespace around
<<is optional:name:[int] << 1, 2andname:[int]<<1, 2are equivalent. - Whitespace around
:in type annotations is not permitted:name:int, notname : int. - Whitespace around
;in map entries is optional:'key' ; valueand'key';valueare equivalent. - Members are separated by commas, newlines, or both (
SEP). At least one separator is required between pack items or between members inside delimited composites. - Consecutive newlines (blank lines) are ignored.
- Indentation is insignificant — cosmetic only.
Block and inline forms are semantically identical. A conforming formatter may freely convert between them:
# Block struct
deploy:{level:str, release:int} = {
'platform'
26
}
# Inline struct
deploy:{level:str, release:int} = { 'platform', 26 }
# Block tuple
version:(int, int, int) = (
3
45
5678
)
# Inline tuple
version:(int, int, int) = (3, 45, 5678)
Type annotations in a PAKT unit are assertions by the producer. They are validated during parsing — a unit that violates its own assertions is malformed.
release:int = 26
status:|active, inactive| = |active
The parser checks each value against its declared type at parse time. A type mismatch is an immediate error.
Status: Design sketch only.
External spec files (.spec.pakt) and consumer projections may be added in a future version. A spec file would use PAKT type syntax without values to define a consumer's requirements. A projection would let a consumer supply a spec at parse time as a filter — parsing only matching fields and skipping the rest without allocation. These features are deferred until real-world usage patterns are better understood.
| Extension | Purpose | MIME Type |
|---|---|---|
.pakt |
PAKT data unit | application/vnd.pakt |
When transmitting PAKT units over a raw byte stream (e.g., pipes, sockets, serial links), implementations MAY use a NUL byte (U+0000, 0x00) as a unit delimiter. The NUL byte is not part of the PAKT text — it is a framing sentinel.
Since NUL is already forbidden in all PAKT text (strings, identifiers, comments), it is unambiguous as a boundary marker.
A parser encountering a NUL byte at the top level MUST treat it as end-of-unit. Behavior on encountering NUL inside a syntactic construct (e.g., mid-string) is already defined as a parse error.
When reading from a file, end-of-file serves as end-of-unit. NUL framing is optional for file-based usage.
A conforming implementation must report parse errors with sufficient detail for programmatic handling and human diagnosis.
Each error MUST include:
- Code — a numeric identifier from the table below (or an implementation-defined code ≥ 100)
- Identifier — a short string name corresponding to the code
- Position — source line and column (1-based)
- Message — a human-readable description of the problem
Codes 1–99 are reserved for the spec. Implementations MUST support at least the categories below and MUST allow callers to distinguish them programmatically (via sentinel errors, error codes, typed exceptions, or equivalent).
| Code | Identifier | Condition |
|---|---|---|
| 1 | unexpected_eof |
Input ends before a syntactic construct is complete |
| 2 | type_mismatch |
A value does not conform to its declared type |
| 3 | nil_non_nullable |
nil appears where the type is not nullable |
| 4 | syntax |
Any lexical or grammatical error not covered by a more specific category |
Implementations MAY define additional error categories with codes ≥ 100. Implementation-defined categories must be documented and must not reuse codes 1–99 for different meanings.
The following conditions are explicitly not parse errors:
- Duplicate root statement names — preserved in encounter order per §6.1 and principle 3 (lossless decoder). Higher-level consumers decide whether duplicates are meaningful.
- Duplicate map keys — preserved in encounter order per §6.2 and principle 3 (lossless decoder). Higher-level consumers decide whether duplicates are meaningful.
Website: usepakt.dev
This appendix assembles every grammar rule from the spec body into a single reference. No new rules are introduced — this is a collected view for implementors.
; --- Characters and whitespace ---
ALPHA = 'a'-'z' | 'A'-'Z'
DIGIT = '0'-'9'
HEX_DIGIT = DIGIT | 'a'-'f' | 'A'-'F'
BIN_DIGIT = '0' | '1'
OCT_DIGIT = '0'-'7'
DIGIT_SEP = DIGIT (DIGIT | '_')*
BASE64_CHAR = ALPHA | DIGIT | '+' | '/' | '='
IDENT = (ALPHA | '_') (ALPHA | DIGIT | '_' | '-')*
WS = ' ' | '\t'
NL = '\n' | '\r\n'
SEP = ',' | NL
COMMENT = '#' (any char except NL)* NL
; --- Tokens ---
ASSIGN = '='
COLON = ':'
SEMI = ';'
PACK = '<<'
COMMA = ','
PIPE = '|'
LBRACE = '{' RBRACE = '}'
LPAREN = '(' RPAREN = ')'
LBRACK = '[' RBRACK = ']'
LANGLE = '<' RANGLE = '>'
; Reserved tokens (§4.5)
AT = '@'
BANG = '!'
STAR = '*'
DOLLAR = '$'
AMP = '&'
TILDE = '~'
BTICK = '`'
; --- Scalar literals ---
INT = ['-'] DIGIT_SEP
| ['-'] '0x' HEX_DIGIT (HEX_DIGIT | '_')*
| ['-'] '0b' BIN_DIGIT (BIN_DIGIT | '_')*
| ['-'] '0o' OCT_DIGIT (OCT_DIGIT | '_')*
DEC = ['-'] DIGIT_SEP? '.' DIGIT_SEP
FLOAT = ['-'] DIGIT_SEP? ('.' DIGIT_SEP)? ('e' | 'E') [+-]? DIGIT+
BOOL = 'true' | 'false'
NIL = 'nil'
DATE = DIGIT{4} '-' DIGIT{2} '-' DIGIT{2}
TZ = 'Z' | [+-] DIGIT{2} ':' DIGIT{2}
TS = DATE 'T' DIGIT{2} ':' DIGIT{2} ':' DIGIT{2} ('.' DIGIT+)? TZ
UUID = HEX_DIGIT{8} '-' HEX_DIGIT{4} '-' HEX_DIGIT{4} '-' HEX_DIGIT{4} '-' HEX_DIGIT{12}
BIN = 'x' "'" HEX_DIGIT* "'"
| 'b' "'" BASE64_CHAR* "'"
; --- String literals ---
ESCAPE = '\' ('\' | "'" | '"' | 'n' | 'r' | 't')
| '\u' HEX_DIGIT{4}
string_char = ESCAPE | any code point except matching_quote, '\', NL, U+0000
raw_char = any code point except matching_quote, U+0000
ml_char = ESCAPE | any code point except U+0000, not forming closing triple-quote
raw_ml_char = any code point except U+0000, not forming closing triple-quote
STRING = "'" string_char* "'" | '"' string_char* '"'
RAW_STR = 'r' "'" raw_char* "'" | 'r' '"' raw_char* '"'
ML_STR = "'''" ml_char* "'''" | '"""' ml_char* '"""'
ML_RAW = "r'''" raw_ml_char* "'''" | 'r"""' raw_ml_char* '"""'
ATOM = PIPE IDENT
; --- Unit structure ---
unit = statement*
statement = assign | pack
assign = IDENT type_annot ASSIGN value
pack = IDENT type_annot PACK pack_body
; --- Pack body ---
; Terminates at end-of-unit (EOF or NUL) or the start of the next statement (IDENT COLON).
pack_body = list_pack_body | map_pack_body
list_pack_body = (value (SEP value)* SEP?)?
map_pack_body = (map_entry (SEP map_entry)* SEP?)?
; --- Type annotations ---
type_annot = COLON type
type = (scalar_type
| atom_set
| struct_type
| tuple_type
| list_type
| map_type) '?'?
scalar_type = 'str' | 'int' | 'dec' | 'float' | 'bool'
| 'uuid' | 'date' | 'ts' | 'bin'
atom_set = PIPE IDENT (COMMA IDENT)* PIPE
struct_type = LBRACE (field_decl (COMMA field_decl)*)? RBRACE
field_decl = IDENT COLON type
tuple_type = LPAREN (type (COMMA type)*)? RPAREN
list_type = LBRACK type RBRACK
map_type = LANGLE type SEMI type RANGLE
; --- Values ---
value = scalar | NIL | atom_val
| struct_val | tuple_val | list_val | map_val
scalar = STRING | RAW_STR | ML_STR | ML_RAW
| INT | DEC | FLOAT | BOOL
| UUID | DATE | TS | BIN
atom_val = ATOM
struct_val = LBRACE (value (SEP value)* SEP?)? RBRACE
tuple_val = LPAREN (value (SEP value)* SEP?)? RPAREN
list_val = LBRACK (value (SEP value)* SEP?)? RBRACK
map_val = LANGLE (map_entry (SEP map_entry)* SEP?)? RANGLE
map_entry = value SEMI value
The following identifiers are reserved and cannot be used as statement names or atom set members:
true false nil