// PEG grammar for the .inkt textual format.
// Matches the output of `write_inkt` exactly.
WHITESPACE = _{ " " | "\t" | NEWLINE }
story = { SOI ~ "(" ~ "story" ~ story_checksum? ~ name_table? ~ globals? ~ lists? ~ list_items? ~ externals? ~ addresses? ~ address_paths? ~ list_literals? ~ literal_pool? ~ struct_shapes? ~ visibility? ~ alias_table? ~ effect_rows? ~ frame_shapes? ~ debug_info? ~ line_variant_groups? ~ container* ~ ")" ~ EOI }
story_checksum = { "checksum=" ~ hex_literal }
// ── Name table ──────────────────────────────────────────────────────────────
name_table = { "(" ~ "name_table" ~ name_entry* ~ ")" }
name_entry = { integer ~ string }
// ── Globals ─────────────────────────────────────────────────────────────────
globals = { "(" ~ "globals" ~ global_entry* ~ ")" }
global_entry = { "(" ~ "global" ~ def_id ~ ":" ~ type_name ~ value ~ mutable_flag? ~ local_flag? ~ "(" ~ "name" ~ integer ~ ")" ~ ")" }
mutable_flag = { "mutable" }
local_flag = { "local" }
// T1c (issue #742): "record" | "fn_ref" | "closure" mirror
// `value_type_name` in `inkt/write.rs` — a global var can legally declare
// any of the three T1c function-value tags as its `value_type`.
type_name = { "int" | "float" | "bool" | "string" | "list" | "divert_target" | "var_pointer" | "null" | "array" | "map" | "record" | "fn_ref" | "closure" | "handle" | "projection" | "option" | "range" | "vec2" | "vec3" | "vec4" | "quat" | "mat2" | "mat3" | "mat4" | "weighted" }
// ── Lists ───────────────────────────────────────────────────────────────────
lists = { "(" ~ "lists" ~ list_entry* ~ ")" }
list_entry = { "(" ~ "list" ~ def_id ~ "(" ~ "name" ~ integer ~ ")" ~ list_item_inline* ~ ")" }
list_item_inline = { "(" ~ "item" ~ "name=" ~ integer ~ "ordinal=" ~ integer ~ ")" }
// ── List items ──────────────────────────────────────────────────────────────
list_items = { "(" ~ "list_items" ~ list_item_entry* ~ ")" }
list_item_entry = { "(" ~ "list_item" ~ def_id ~ "(" ~ "origin" ~ def_id ~ ")" ~ "(" ~ "ordinal" ~ integer ~ ")" ~ ("(" ~ "name" ~ integer ~ ")")? ~ ")" }
// ── Externals ───────────────────────────────────────────────────────────────
externals = { "(" ~ "externals" ~ extern_entry* ~ ")" }
extern_entry = { "(" ~ "extern" ~ def_id ~ "argc=" ~ integer ~ "(" ~ "name" ~ integer ~ ")" ~ fallback? ~ ")" }
fallback = { "(" ~ "fallback" ~ def_id ~ ")" }
// ── Addresses ───────────────────────────────────────────────────────────────
addresses = { "(" ~ "addresses" ~ address_entry* ~ ")" }
address_entry = { "(" ~ "address" ~ def_id ~ "->" ~ def_id ~ "+" ~ integer ~ ")" }
// ── Address paths ─────────────────────────────────────────────────────────────
address_paths = { "(" ~ "address_paths" ~ address_path_entry* ~ ")" }
address_path_entry = { "(" ~ "path" ~ integer ~ "->" ~ def_id ~ ")" }
// ── Alias table (M-3, docs/modules-spec.md §5) ────────────────────────────────
alias_table = { "(" ~ "alias_table" ~ alias_entry* ~ ")" }
alias_entry = { "(" ~ "alias" ~ def_id ~ "->" ~ def_id ~ ")" }
// ── Effect rows (T2-3, docs/effects-spec.md §11) ─────────────────────────────
effect_rows = { "(" ~ "effect_rows" ~ effect_row* ~ ")" }
effect_row = { "(" ~ "row" ~ def_id ~ internal_flag? ~ effects_reads ~ effects_writes ~ effects_calls ~ opaque_flag? ~ emits_flag? ~ tags_flag? ~ faults_flag? ~ dispatch_entry* ~ ")" }
dispatch_entry = { "(" ~ "dispatch" ~ def_id ~ narrowable_flag? ~ effects_reads ~ effects_writes ~ effects_calls ~ opaque_flag? ~ emits_flag? ~ tags_flag? ~ faults_flag? ~ ")" }
// ── Frame shapes (FS-3, docs/flow-suspension-spec.md §4/§11) ──────────────────
// One entry per `await` site: the site's stable def_id (the synthesized
// continuation container id) followed by its name-keyed crossing-local slots.
frame_shapes = { "(" ~ "frame_shapes" ~ frame_shape_entry* ~ ")" }
// Stage 1 of the shared-alternatives track (#3273): variant-group records
// tying runs of line-table entries back to one authored line.
// group: scope_id base (dims d0 d1 ...)
line_variant_groups = { "(" ~ "line_variant_groups" ~ line_variant_group_entry* ~ ")" }
line_variant_group_entry = { "(" ~ "group" ~ def_id ~ integer ~ "(" ~ "dims" ~ integer+ ~ ")" ~ ")" }
frame_shape_entry = { "(" ~ "frame" ~ def_id ~ integer* ~ ")" }
// ── DebugInfo (D6, docs/debugger-spec.md §2) ──────────────────────────────────
// Emitted only when debug info was requested at compile time — omitted from
// the story text entirely, not just empty, when the section is absent.
debug_info = { "(" ~ "debug_info" ~ debug_files? ~ debug_container* ~ ")" }
debug_files = { "(" ~ "files" ~ debug_file_entry* ~ ")" }
// file: idx surface "path" source_hash (lines start*)? [#3261]
debug_file_entry = { "(" ~ "file" ~ integer ~ debug_surface ~ string ~ integer ~ debug_file_lines? ~ ")" }
debug_file_lines = { "(" ~ "lines" ~ integer* ~ ")" }
debug_surface = { "synthetic" | "ink" | "native" }
debug_container = { "(" ~ "dcontainer" ~ integer ~ debug_entry* ~ debug_locals? ~ ")" }
// entry: bytecode_offset file_idx range_start range_len kind_token flags
debug_entry = { "(" ~ "entry" ~ integer ~ integer ~ integer ~ integer ~ integer ~ integer ~ ")" }
debug_locals = { "(" ~ "locals" ~ debug_local_entry* ~ ")" }
// local: slot "name" synthetic? (range file_idx start len)? [#3395: synthetic]
debug_local_entry = { "(" ~ "local" ~ integer ~ string ~ debug_synthetic? ~ debug_range? ~ ")" }
debug_synthetic = { "synthetic" }
debug_range = { "(" ~ "range" ~ integer ~ integer ~ integer ~ ")" }
effects_reads = { "(" ~ "reads" ~ def_id* ~ ")" }
effects_writes = { "(" ~ "writes" ~ def_id* ~ ")" }
effects_calls = { "(" ~ "calls" ~ call_atom* ~ ")" }
call_atom = { "(" ~ "call" ~ integer ~ cap_param ~ ")" }
cap_param = { "any" }
opaque_flag = { "opaque" }
emits_flag = { "emits" }
tags_flag = { "tags" }
faults_flag = { "faults" }
narrowable_flag = { "narrowable" }
// #882 freeze bit: present only when the row is NOT a host entry point
// (a `#@private` def — see `EffectRowEntry::is_entry`'s doc). Absent means
// `is_entry = true`, the default for every public knot/stitch.
internal_flag = { "internal" }
// ── List literals ────────────────────────────────────────────────────────────
list_literals = { "(" ~ "list_literals" ~ list_literal_entry* ~ ")" }
list_literal_entry = { "(" ~ "list" ~ list_value_items ~ list_value_origins ~ ")" }
// ── Literal pool (T1b, docs/format-v4-rfc.md §2) ─────────────────────────────
literal_pool = { "(" ~ "literal_pool" ~ value* ~ ")" }
// ── Struct shapes (TM-4, docs/format-v4-rfc.md §1) ───────────────────────────
// Mirrors the `.inkb` `StructShapes` section: shape id, name, then its
// ordered field `NameId`s. Reader lands with the writer in the same PR
// (the #742/#883 lesson — this was previously write-only through `.inkb`
// only, with `.inkt` dropping the section entirely).
struct_shapes = { "(" ~ "struct_shapes" ~ struct_shape_entry* ~ ")" }
struct_shape_entry = { "(" ~ "struct" ~ integer ~ "(" ~ "name" ~ integer ~ ")" ~ struct_field* ~ ")" }
struct_field = { "(" ~ "field" ~ integer ~ ")" }
// ── Visibility (M-2b, docs/modules-spec.md §4) ───────────────────────────────
visibility = { "(" ~ "visibility" ~ private_entry* ~ ")" }
private_entry = { "(" ~ "private" ~ def_id ~ ")" }
// ── Containers ──────────────────────────────────────────────────────────────
container = { "(" ~ "container" ~ def_id ~ hash_field? ~ scope_field? ~ container_name_field? ~ flags_field? ~ path_hash_field? ~ params_field? ~ local_flag? ~ lines_field? ~ code_field? ~ ")" }
scope_field = { "(" ~ "scope" ~ def_id ~ ")" }
container_name_field = { "(" ~ "name" ~ integer ~ ")" }
path_hash_field = { "(" ~ "path_hash" ~ integer ~ ")" }
params_field = { "(" ~ "params" ~ integer ~ param_meta* ~ ")" }
param_meta = { "(" ~ param_mode ~ integer ~ integer ~ ")" }
param_mode = { "val" | "ref" }
hash_field = { "(" ~ "hash" ~ hex_literal ~ ")" }
flags_field = { "(" ~ "flags" ~ flag_name+ ~ ")" }
flag_name = { "visits" | "turns" | "start_only" | "invisible" }
lines_field = { "(" ~ "lines" ~ line_entry* ~ ")" }
line_entry = { integer ~ line_content ~ source_hash ~ audio_field? ~ slots_field? ~ source_field? }
audio_field = { "(" ~ "audio" ~ string ~ ")" }
slots_field = { "(" ~ "slots" ~ slot_entry+ ~ ")" }
slot_entry = { integer ~ ":" ~ string }
source_field = { "(" ~ "source" ~ string ~ integer ~ ".." ~ integer ~ ")" }
source_hash = @{ "@" ~ ASCII_HEX_DIGIT{16} }
line_content = { string | template }
template = { "(" ~ "template" ~ template_part+ ~ ")" }
template_part = { literal_part | slot_part | select_part | span_part }
literal_part = { "(" ~ "lit" ~ string ~ ")" }
slot_part = { "(" ~ "slot" ~ integer ~ ")" }
select_part = { "(" ~ "select" ~ "slot=" ~ integer ~ select_variant* ~ select_default ~ ")" }
span_part = { "(" ~ "span" ~ string ~ span_attr* ~ template_part* ~ ")" }
span_attr = { "(" ~ "attr" ~ string ~ string ~ ")" }
select_variant = { "(" ~ select_key ~ string ~ ")" }
select_default = { "(" ~ "default" ~ string ~ ")" }
select_key = { cardinal_key | ordinal_key | exact_key | keyword_key }
cardinal_key = { "cardinal:" ~ plural_cat }
ordinal_key = { "ordinal:" ~ plural_cat }
exact_key = { "=" ~ integer }
keyword_key = { "keyword:" ~ ident }
plural_cat = { "Zero" | "One" | "Two" | "Few" | "Many" | "Other" }
// ── Code ────────────────────────────────────────────────────────────────────
code_field = { "(" ~ "code" ~ instruction* ~ ")" }
instruction = { opcode_mnemonic ~ operand* }
opcode_mnemonic = @{ ASCII_ALPHA ~ (ASCII_ALPHANUMERIC | "_")* }
operand = { choice_flags | kv_operand | def_id | float | integer | seq_kind | bool_lit }
kv_operand = @{ ASCII_ALPHA+ ~ "=" ~ (ASCII_ALPHANUMERIC | "_" | "-")+ }
// Atomic with a trailing word-boundary guard (#3273): a bare `"shuffle"`
// literal would otherwise prefix-match the `shuffle_index_of` MNEMONIC on
// the next line as a trailing operand of the previous instruction.
seq_kind = @{ ("cycle" | "stopping" | "once_only" | "shuffle") ~ !(ASCII_ALPHANUMERIC | "_") }
bool_lit = { "true" | "false" }
choice_flags = @{ ("cond" | "start" | "choice_only" | "once" | "invis_default" | "none") ~ ("+" ~ ("cond" | "start" | "choice_only" | "once" | "invis_default"))* }
// ── Values ──────────────────────────────────────────────────────────────────
value = { array_value | map_value | list_value | var_pointer_value | fragment_ref_value | handle_value | projection_value | record_value | fn_ref_value | closure_value | option_value | range_value | tower_value | weighted_value | string | float | integer | def_id | null_value | bool_value }
var_pointer_value = { "(" ~ "var_pointer" ~ def_id ~ ")" }
fragment_ref_value = { "(" ~ "fragment_ref" ~ integer ~ ")" }
// T1d handle value (docs/t1d-spec.md §2, docs/format-v4-rfc.md §1) — the
// textual mirror of the `VAL_HANDLE` wire tag: kind NameId, then u64 id.
// Reader lands with the writer in the same PR (the #742 asymmetry class —
// record/fn_ref/closure atoms are write-only; this one is not).
handle_value = { "(" ~ "handle" ~ integer ~ integer ~ ")" }
// T1e projection value (docs/t1e-spec.md §3, docs/format-v4-rfc.md §1) — the
// textual mirror of the `VAL_PROJECTION` wire tag: cell reference, then an
// ordered segment list. Reader lands with the writer in the same PR (the
// #742 lesson, same discipline `handle_value` follows).
projection_value = { "(" ~ "projection" ~ def_id ~ proj_segments ~ ")" }
proj_segments = { "(" ~ "segments" ~ proj_segment* ~ ")" }
proj_segment = { index_segment | key_segment }
index_segment = { "(" ~ "index" ~ integer ~ ")" }
key_segment = { "(" ~ "key" ~ value ~ ")" }
list_value = { "(" ~ "list" ~ list_value_items ~ list_value_origins ~ ")" }
// NS-A1 Option value (docs/stdlib-spec.md §1.4) — the textual mirror of the
// VAL_OPTION wire tag: `(some <value>)` / `(option_none)`. Reader lands with
// the writer in the same PR (the #742 lesson, same discipline handle_value
// follows). Spelled `option_none` rather than bare `none` because `none` is
// already a choice-flags token above.
option_value = { some_value | none_value }
some_value = { "(" ~ "some" ~ value ~ ")" }
none_value = { "(" ~ "option_none" ~ ")" }
// NS-A5 range value (docs/stdlib-spec.md §7, F7) — the textual mirror of
// the VAL_RANGE wire tag: `(range <start> <end> incl|excl)`. The written
// form (`..=` vs `..`) is preserved through the incl/excl token; reader
// lands with the writer in the same PR (the #742 lesson).
range_value = { "(" ~ "range" ~ integer ~ integer ~ range_form ~ ")" }
range_form = { "incl" | "excl" }
// NS-A8 tower values (docs/tower-mini-spec.md T5) — the textual mirror of
// the VAL_VEC2..VAL_MAT4 wire tags: flat f32 lanes in the pinned order
// (vec/quat `x y (z w)`; matrices column-major, column-by-column). Reader
// lands with the writer in the same PR (the #742 dump/reader parity
// lesson). NaN/infinity lanes share `float`'s pre-existing textual
// limitation (the atom is digits-dot-digits) — same posture as a bare
// float value, not a new one.
tower_value = { vec2_value | vec3_value | vec4_value | quat_value | mat2_value | mat3_value | mat4_value }
vec2_value = { "(" ~ "vec2" ~ float ~ float ~ ")" }
vec3_value = { "(" ~ "vec3" ~ float ~ float ~ float ~ ")" }
vec4_value = { "(" ~ "vec4" ~ float ~ float ~ float ~ float ~ ")" }
quat_value = { "(" ~ "quat" ~ float ~ float ~ float ~ float ~ ")" }
mat2_value = { "(" ~ "mat2" ~ float{4} ~ ")" }
mat3_value = { "(" ~ "mat3" ~ float{9} ~ ")" }
mat4_value = { "(" ~ "mat4" ~ float{16} ~ ")" }
// NS-A7 weighted tables (docs/stdlib-spec.md §8) — the textual mirror of
// the VAL_WEIGHTED wire tag: `(weighted (<weight> <value>)+)`, entries in
// construction order (order is semantic for display and the roll walk).
// Reader lands with the writer in the same PR (the #742 dump/reader parity
// lesson). The non-empty/positive-weight invariant is enforced by the
// reader, not the grammar (a targeted error message beats a parse failure).
weighted_value = { "(" ~ "weighted" ~ weighted_entry* ~ ")" }
weighted_entry = { "(" ~ integer ~ value ~ ")" }
list_value_items = { "(" ~ "items" ~ def_id* ~ ")" }
list_value_origins = { "(" ~ "origins" ~ def_id* ~ ")" }
null_value = { "null" }
bool_value = { "true" | "false" }
// T1b collection values (docs/value-model-spec.md §4, docs/format-v4-rfc.md
// §1) — the textual mirror of the `VAL_ARRAY`/`VAL_MAP` tree encoding.
array_value = { "(" ~ "array" ~ value* ~ ")" }
map_value = { "(" ~ "map" ~ map_entry* ~ ")" }
map_entry = { "(" ~ map_key ~ value ~ ")" }
map_key = { string | integer | bool_value }
// T1c function values (docs/t1c-spec.md §1/§6, docs/format-v4-rfc.md §4) —
// the textual mirror of `VAL_RECORD`/`VAL_FN_REF`/`VAL_CLOSURE`. Must stay in
// sync with `write_value` in `inkt/write.rs` (dump-parity rule, issue #742).
record_value = { "(" ~ "record" ~ integer ~ value* ~ ")" }
fn_ref_value = { "(" ~ "fn_ref" ~ def_id ~ ")" }
closure_value = { "(" ~ "closure" ~ def_id ~ closure_entry* ~ ")" }
closure_entry = { "(" ~ param_mode ~ integer ~ value ~ ")" }
// ── Primitives ──────────────────────────────────────────────────────────────
def_id = @{ "$" ~ ASCII_HEX_DIGIT{2} ~ "_" ~ ASCII_HEX_DIGIT+ }
hex_literal = @{ "0x" ~ ASCII_HEX_DIGIT+ }
string = @{ "\"" ~ (escape | (!("\"" | "\\") ~ ANY))* ~ "\"" }
escape = { "\\\\" | "\\\"" | "\\n" | "\\t" | "\\r" }
integer = @{ "-"? ~ ASCII_DIGIT+ }
float = @{ "-"? ~ ASCII_DIGIT+ ~ "." ~ ASCII_DIGIT* }
ident = @{ (ASCII_ALPHANUMERIC | "_")+ }