fig-sys 5.0.0

FFI bindings and native library for fig (the comment-preserving JSON/YAML/TOML/… config engine). Used by the `fig` crate.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
//! The declared half of the `Language` interface — what a format supplies
//! about its own syntax, rather than what `Editor` infers by testing which
//! language it was instantiated with.
//!
//! `language.zig`'s `validate` states the four-declaration contract (`Type`,
//! `default_type`, `parse`, `print`). That was never the whole of it: the rest
//! lived as `if (Language == Toml)` / `== Plist` / `!= NestedText` branches
//! inside `editor.zig`, discoverable only by reading them. The types here are
//! where that unwritten half becomes writable — a format declares `syntax`,
//! `caps`, `name` and `extensions`, and the generic engine reads them instead
//! of asking what it is. See `docs/proposals/language-interface.md`.
//!
//! This module is the PARAMETER half of that interface — the answers a format
//! can give as a value. The other half is how a format SPELLS a fragment the
//! engine cannot: the renderers, declared in the "Renderers" block of a
//! `<lang>/<lang>.zig` and dispatched by `@hasDecl` from `editor.zig`; they
//! need no types here, because each is a pure function from strings to a
//! string whose signature is fixed by the `Editor` method that calls it
//! (`Decls.renderers` in `language.zig` lists them).
//!
//! It also holds the shape of a format-registry entry, `Dialect`, since each
//! language now declares its own dialects (`Language.dialects`) and
//! `language.zig` only assembles them.
//!
//! This module is deliberately a LEAF: it imports nothing, not even `std`.
//! `language.zig` re-exports these types and every `<lang>/<lang>.zig` imports
//! them, so anything pulled in here would be pulled into all eleven language
//! modules — and an import back to `language.zig` (which imports each of them
//! in turn) would make the manifest's own types depend on the languages that
//! declare them.

/// Which leading-comment syntax a language uses, so the owned-comment scan in
/// delete/move (`editor/splice.zig`'s `commentBlockStart`) recognizes the right marker.
///
/// Distinct from `Comments.line`: this selects the *scanner*, which is
/// per-language and comptime, while the marker is per-dialect and may be null
/// where the scanner still has a sensible answer. Plain JSON has no comments,
/// but `.slashes` is harmless there since no `//` line can exist.
pub const CommentStyle = enum {
    /// YAML, TOML, fig, dotenv, `.properties`, NestedText.
    hash,
    /// JSON/JSONC/JSON5 and ZON (which follows Zig). The only style whose
    /// scanner also walks multi-line `/* ... */` blocks as a unit.
    slashes,
    /// INI. Its printer accepts a leading `#` on read but always WRITES `;`,
    /// so `;` is the marker the editor's own inserts and scans use.
    semicolon,
    /// plist's `<!-- ... -->`.
    xml_comment,
};

/// A format's whole comment surface: which scanner walks an owned comment
/// block, and the markers the editor writes and strips.
///
/// One field on `Syntax` rather than three, because the three answers coincide
/// for most formats and restating the same marker three times reads as
/// redundancy rather than as the three independent questions it is. The two
/// presets below are exactly the "all three agree" case; a format whose
/// answers diverge — INI, NestedText, plist — writes the literal out, and so
/// does JSON, whose marker varies by dialect while its scanner does not.
/// How one comment is delimited: the token that opens it and, for a paired
/// syntax, the one that closes it. `#`, `//` and `;` are an `open` alone;
/// plist's `<!-- … -->` is a pair. The engine writes a comment as
/// `open`, a space, the text, and — when `close` is non-empty — a space and
/// `close`; it strips the same shape when reading one back, and it finds a
/// trailing comment by searching for `open`. A pair used to be inexpressible
/// here, which is why plist hooked all six leading/trailing comment ops.
pub const CommentDelimiter = struct {
    open: []const u8,
    close: []const u8 = "",
    /// Text a comment body may not contain — `--` inside an XML comment —
    /// refused as `InvalidComment` before anything is spliced.
    forbidden: ?[]const u8 = null,
};

pub const Comments = struct {
    /// Selects the owned-comment-block scanner. See `CommentStyle`. Never
    /// null, and never redundant with `line`: the scanner is per-language
    /// while a marker is per-dialect, so strict JSON declares `.slashes`
    /// alongside a null marker — unobservable there, since no `//` line can
    /// exist for the scanner to find.
    style: CommentStyle,

    /// The own-line (leading) comment delimiter, or null when the dialect has
    /// none to write — strict JSON, where the comment ops return
    /// `CommentsUnsupported`. A paired delimiter (plist) serves the leading
    /// and trailing ops; the dangling and comment-out ops need a bare prefix
    /// and refuse a pair.
    line: ?CommentDelimiter,

    /// The marker for a same-line TRAILING comment specifically, or null when
    /// the format has no such syntax.
    ///
    /// Distinct from `line` because INI and NestedText have real, safe leading
    /// comments but no trailing ones: a `;`/`#` after a value on the SAME line
    /// is literal value text, not a comment (see `ini/parser.zig`'s "a value
    /// runs to end of line" and `nestedtext/parser.zig`'s "rest-of-line values
    /// are 100% literal", and both printers, which render a "trailing" comment
    /// as its own line immediately after the entry). Splicing one in anyway
    /// would silently corrupt the value on reread, so trailing ops are refused
    /// there.
    trailing: ?CommentDelimiter,

    /// `#` throughout — YAML, TOML, fig, dotenv, `.properties`.
    pub const hash: Comments = .{ .style = .hash, .line = .{ .open = "#" }, .trailing = .{ .open = "#" } };

    /// `//` throughout — ZON, which follows Zig.
    pub const slashes: Comments = .{ .style = .slashes, .line = .{ .open = "//" }, .trailing = .{ .open = "//" } };
};

/// How a logical mapping key renders as this format's key syntax on the `set`
/// insert path (`editor.formatInsertKey`).
///
/// A logical key is plain text (`b`, `has space`); what reaches the source
/// depends on the format's key grammar, and the splice is reparsed under it.
pub const KeyStyle = enum {
    /// Spliced as-is — YAML, TOML, fig, INI, dotenv, `.properties`,
    /// NestedText, plist. The same thing `insertKey`'s other callers do.
    verbatim,
    /// Quoted and escaped as a JSON string (`b` -> `"b"`). Required by strict
    /// JSON and harmless in JSONC/JSON5.
    json_quoted,
    /// ZON's struct-field syntax, which always carries a leading `.`
    /// (`b` -> `.b`, quoted as `.@"has space"` when not a bare identifier).
    zon_field,
    /// Bare when every byte is `[A-Za-z0-9_-]`, else a basic-quoted string
    /// with `"` and `\` escaped — TOML's key rule, used for a header path's
    /// segments and a renamed table's leaf as well as an inserted key.
    bare_or_quoted,
};

/// How a section format spells a header line that opens a container of its
/// own: `[` + path + `]` for a TOML table, `[[` + path + `]]` for an element
/// of an array of tables, an INI `[section]`. See `Syntax.section_header`.
pub const SectionHeader = struct {
    open: []const u8,
    close: []const u8,
    /// The element-of-a-sequence form, or null for a format without one.
    seq_open: ?[]const u8 = null,
    seq_close: ?[]const u8 = null,
    /// Joins the path's key segments, each rendered per `key_style`.
    sep: []const u8 = ".",
    /// Whether index segments are left out of the path — `[[a.b]]` always
    /// names `a`'s last element, so the index is implied.
    skip_index: bool = true,
};

/// What `fig` can do with a format, as declared by the format itself.
///
/// The single source: `c_api.fig_format_capabilities` reads these bits rather
/// than restating them, so the C ABI cannot disagree with the format about what
/// the format can do. It used to be hand-maintained in both places, and drifted
/// silently in both directions — see the proposal's §7 and §12.
///
/// The MAPPING between the two is not spelled out anywhere either: `FigFormat`
/// is per-dialect (json/jsonc/json5 are three ABI values over this one
/// `Language`) while `caps` is per-language, and the format registry's `Lang`
/// field is the bridge — `fig_format_capabilities` looks the entry up by member
/// name and reads the bits off the language it names.
pub const Caps = struct {
    /// `parse` accepts this format. True for every language in tree.
    read: bool = true,
    /// `Editor(Language)` is instantiated for this format. True for every
    /// language in tree since generic XML (a reader and a writer with no
    /// in-place editor) was removed in core 3.0; an out-of-tree `Language`
    /// may still declare false, and the CLI and C ABI refuse to edit it.
    edit: bool = false,
    /// `print` can write this format.
    serialize: bool = false,
    /// This format has a reference layer: its `parse` may return anchors,
    /// aliases, `<<` merges and tags, and its `print` spells them. A
    /// document carrying one is written out intact when the target declares
    /// this too, and collapsed by `Materialize` first when it does not —
    /// which is the whole of how the CLI and the C ABI decide to run that
    /// pass, so a language that produces alias rows without declaring this
    /// hands another format's printer an alias it cannot spell. YAML alone
    /// in tree; a runtime language declares it as `caps.references`.
    references: bool = false,
    /// What the lossless `$fig` envelope pass (`lossless.zig`) may assume
    /// about this format's value model on OUTPUT, or null when the format
    /// takes no envelope at all.
    ///
    /// Non-null says: when `--lossless` targets this format, wrap every
    /// scalar kind NOT marked native in `NativeKinds` in a `$fig` envelope so
    /// a later run can rebuild it, and leave the marked kinds bare. Only the
    /// four typed formats with a real value model and a mapping to carry the
    /// envelope in — JSON, YAML, TOML, ZON — declare one.
    ///
    /// Null says: never encode an envelope into this format (envelopes in
    /// its INPUT are still decoded). Two distinct reasons collapse into the
    /// one answer, deliberately, because the pass has one behaviour for both:
    ///
    ///   * fig and canonical spell every kind directly, so an envelope would
    ///     preserve nothing a plain print does not.
    ///   * INI, dotenv, `.properties`, plist and NestedText have no typed
    ///     scalar envelope of their own — their printers already reduce the
    ///     value to text, so a mapping-shaped envelope would be no more
    ///     recoverable than the degraded scalar it replaced.
    ///
    /// A field on `Caps` rather than its own `Language` declaration because it
    /// IS a capability — "can fig round-trip a value through this format
    /// without loss, and which values need help" — and because the seven
    /// null answers then cost nothing to state: the default is the
    /// conservative one. It sits on the LANGUAGE (json/jsonc/json5 share it),
    /// which is why JSON5's native `Infinity`/`NaN` are still enveloped: the
    /// declaration is per-language and JSON's is the strict dialect's.
    lossless: ?NativeKinds = null,

    /// How many levels of mapping nesting this format can represent, or
    /// null for a format with no depth limit at all (every typed format).
    /// INI holds a root mapping plus one level of `[section]`s (1); dotenv
    /// and `.properties` are flat — the root mapping itself, nothing nested
    /// under it (0).
    ///
    /// A non-null value also says the format holds no sequence anywhere and
    /// no `null`: the three flat formats share that shape, and
    /// `flat_strip.zig` — the lossy pass that drops what such a format
    /// cannot hold before printing — and `diagnostics.zig`'s matching
    /// warning read this one field for the depth and take the rest as
    /// given. A future shallow format with sequences would need a second
    /// field, not a different reading of this one.
    max_mapping_depth: ?u8 = null,
};

/// The scalar kinds a format spells natively, beyond the core four every
/// serialize format has (boolean, string, number, and the two containers).
/// Read by `lossless.zig`, whose `needsEnvelope` is exactly "the kind is one
/// of these and the format did not mark it".
///
/// One field per kind the envelope can carry: `null`, plus one per
/// `AST.Node.Kind.Extended.ExtKind` member, named identically. This module is
/// a leaf and cannot name the AST's enum, so the correspondence is a
/// comptime pin in `lossless.zig` (both directions) rather than a type: a new
/// `ExtKind` fails the build until a field for it exists here, and a field
/// with no `ExtKind` behind it fails the same way. Every field defaults to
/// false — a format declares what it holds, and an omission is "envelope it",
/// which is always lossless if sometimes unidiomatic.
pub const NativeKinds = struct {
    /// A bare `null`. Every typed format but TOML has one; TOML's absence is
    /// the one kind the lossy path (`Lossless.lossyStrip`) DROPS rather than
    /// degrades, since there is no string to collapse it to.
    null: bool = false,
    /// The four RFC-3339-derived TOML datetimes.
    offset_datetime: bool = false,
    local_datetime: bool = false,
    local_date: bool = false,
    local_time: bool = false,
    /// ZON's `.name` and `'c'` literals.
    enum_literal: bool = false,
    char_literal: bool = false,
    /// A non-finite float (`inf`/`nan`, JSON5's `Infinity`/`NaN`).
    number_special: bool = false,
    /// plist's `<date>` and `<data>`.
    plist_date: bool = false,
    plist_data: bool = false,
};

/// How a format spells itself when it is EMBEDDED in a host document — the
/// four openers `embed.zig` writes, and the tags/MIMEs it accepts on read.
///
/// Plain data, and deliberately so: this module is a leaf (see the header),
/// and these are strings a format knows about itself, not behaviour. They sit
/// here rather than on `Language` because only four of the eleven formats have
/// an embedded spelling at all — a `Language` decl would either be optional
/// (and so invisible to `Decls`' closed set) or a lie for the other seven. The
/// registry entry in `language.zig` carries `?EmbedSpellings`, and null is the
/// statement that the format has no embedded form.
///
/// These fields ARE `embed.zig`'s spelling tables: its four literal builders
/// (`fencedLiteral`/`frontmatterLiteral`/`scriptLiteral`/`codeLiteral`) and its
/// two resolvers (`formatFromLangTag`/`formatFromScriptMime`) are one
/// `inline`-over-the-registry each, so a value changed here changes the bytes
/// a host document is written with and the spellings it is read back from.
pub const EmbedSpellings = struct {
    /// The ```` ```<tag> ```` info-string this format writes for a fenced
    /// block, WITHOUT the backticks — `embed.zig`'s `fencedLiteral` is
    /// ```` "```" ++ fence_tag ````. Also the canonical spelling
    /// `formatFromLangTag` resolves.
    fence_tag: []const u8,

    /// Extra `<tag>` spellings `formatFromLangTag` accepts for this format on
    /// READ but never writes — `yml` for YAML, `figl` for fig. Matched
    /// case-insensitively, like the canonical tag.
    fence_aliases: []const []const u8 = &.{},

    /// The WHOLE `---<lang>` frontmatter opener, not just the tag: YAML's is a
    /// bare `---` (the ecosystem default — an untagged frontmatter block IS
    /// YAML), while every other format tags it. The one field here that is a
    /// literal rather than a token, because that asymmetry has no token to
    /// carry it. `frontmatterLiteral` emits it verbatim.
    frontmatter: []const u8,

    /// The `type` attribute an `html_script` block is written with —
    /// `scriptLiteral` is `<script type="` ++ script_mime ++ `">`. Also
    /// `formatFromScriptMime`'s canonical arm.
    script_mime: []const u8,

    /// Extra `type` MIMEs `formatFromScriptMime` accepts on READ but never
    /// writes — `application/x-yaml`/`text/yaml`, `application/ld+json`,
    /// `application/fig`. Matched case-insensitively.
    script_mime_aliases: []const []const u8 = &.{},

    /// The `class` token an `html_code` block is written with —
    /// `codeLiteral` is `<pre><code class="` ++ code_class ++ `">`. Note fig's
    /// is `language-figl` while its fence tag is `fig`: the two spellings
    /// genuinely differ. On READ the class token's `language-`/`lang-`
    /// suffix is resolved by `formatFromLangTag`, so `fence_aliases` covers
    /// reading and this covers writing.
    code_class: []const u8,
};

/// `Lang.Type` when `Lang` is a language, `void` when it is the gated-out
/// placeholder.
///
/// A `-D<lang>=false` build resolves that language to `void` in
/// `language.zig`, and `void` has no `.Type` — so a field naming one directly
/// fails to compile in exactly the builds the flag exists to produce. Routing
/// the type through here keeps every dependent shape (a registry `Dialect`,
/// the CLI's `Spec`) identical in every build: the gated-out field becomes a
/// zero-bit `void` that nothing reads, because every consumer already sits
/// behind the same `build_options` test.
pub fn DialectOf(comptime Lang: type) type {
    return if (Lang == void) void else Lang.Type;
}

/// `Lang.default_type`, or the `void` value when `Lang` is gated out.
pub fn defaultDialect(comptime Lang: type) DialectOf(Lang) {
    return if (Lang == void) {} else Lang.default_type;
}

/// How a format takes the caller's edit text, which decides what the fix is
/// when the text turns out not to fit. The semantic `cli/diag_report.zig`'s
/// `spliceStyle` states (and which `cli/edit_ops.zig` acts on), declared once
/// per dialect beside everything else about it.
pub const SpliceStyle = enum {
    /// Spliced in verbatim as source, so a string value needs its own quotes —
    /// YAML, TOML, ZON, fig.
    literal,
    /// Wrapped as a JSON string first (`edit_ops.jsonifyEdit`), so `"`/`\` in
    /// the text are escaped rather than taken as syntax — the JSON family.
    json_string,
    /// Written as raw characters, so only the format's own separators can
    /// break it — INI, dotenv, `.properties`, plist, NestedText. (plist and
    /// NestedText *render* the text rather than splicing it.)
    raw,
};

/// One `--spec <version>` string and the dialect it selects. The element type
/// of `Dialect.specs`, generic over the language so a gated-out one collapses
/// to a `void` dialect and the registry still compiles (and still lists the
/// version STRINGS, which are build-invariant — `resolveSpec` rejects them
/// for a gated-out language rather than not knowing them).
pub fn SpecName(comptime Lang: type) type {
    return struct {
        /// The accepted `--spec` text, matched exactly. Several map to one
        /// dialect (`1.0` and `1.0.0` both select TOML 1.0).
        name: []const u8,
        dialect: DialectOf(Lang),
    };
}

/// One user-facing dialect of a language: everything about it that is not
/// the language module itself. A language declares its own as
/// `Language.dialects: []const Dialect(Language)` — one entry for most, three
/// for JSON (json/jsonc/json5 share one `Language`) — and `language.zig`
/// assembles the format registry from those tables, in language order.
///
/// Generic over the language so the `void` protocol survives (see
/// `DialectOf`): in the registry a gated-out language's entries are
/// `Dialect(void)`, still present with their names, ABI values and spellings,
/// so every enum derived from the registry is build-invariant. A language's
/// own table is always `Dialect(Language)` — it is the registry that lifts
/// the entries to the gated type.
pub fn Dialect(comptime L: type) type {
    return struct {
        /// The member name this dialect has in every derived enum, and —
        /// upper-cased — the `FIG_FORMAT_<NAME>` suffix in fig.h. Sentinel-
        /// terminated because a reified enum's field names must be. The
        /// entry that selects the language's `default_type` must be named
        /// `Language.name` (`validate` checks); the others are the language's
        /// business (JSON's `jsonc`/`json5`).
        name: [:0]const u8,

        /// The language this dialect is a dialect OF; `void` when that
        /// language is gated out of this build. Every consumer must test this
        /// FIRST — it is the gate, and reading any other `Lang`-derived field
        /// past a `void` is a compile error, which is the point.
        Lang: type = L,

        /// The `Lang.Type` value this dialect selects. Defaults to the
        /// language's own default; only the JSON trio overrides it.
        dialect: DialectOf(L) = defaultDialect(L),

        /// The `FigFormat` value in the C ABI. FROZEN: a released value can
        /// never change or be reused, so a new dialect takes the next unused
        /// integer (which is why these run 1,2,7 down the JSON family — JSON5
        /// arrived after generic XML took 6, a value that stays retired now
        /// that the format is gone). `zig build abi-check` compares these against
        /// fig.h's `FIG_FORMAT_*` enumerators in both directions, and
        /// `language.zig` refuses a duplicate.
        abi_value: c_int,

        /// Where this dialect sits in `Language.detect`'s probe order, or
        /// null for a dialect `detect` never sniffs (and which is then no
        /// member of `Detected`) — `jsonc` alone today, since it overlaps
        /// json/json5 on almost all input.
        ///
        /// The order is one argument about grammar overlap — strictest first,
        /// so a permissive grammar cannot claim what a stricter one would
        /// have accepted — and each row carries its own place in it with the
        /// reasoning beside the number. Ranks are unique across the registry
        /// (`language.zig` refuses a duplicate) and need not be contiguous;
        /// `language.zig` pins the resulting sequence in a test, so a new
        /// format choosing a rank cannot reorder the existing ones unnoticed.
        /// Every language must give at least one of its dialects a rank.
        sniff_rank: ?u8 = null,

        /// Whether `deserialize.Format` covers it — the typed
        /// struct-deserialization entry points, which today reach five of the
        /// thirteen dialects.
        deserializable: bool = false,

        /// How this dialect takes spliced edit text. See `SpliceStyle`.
        splice: SpliceStyle,

        /// The document `set` seeds when the target file does not exist yet
        /// (and what `Embed.initRegion` writes into a freshly created region),
        /// or null for a format that refuses to be created from scratch.
        ///
        /// An empty string is NOT the same statement as null: it means an
        /// empty file already parses as an empty root mapping, so the first
        /// key can just be inserted into it.
        empty_doc_seed: ?[]const u8,

        /// The `Lang.Printer` declaration that writes a whole document in this
        /// dialect, and the one that writes a single node. Two names rather
        /// than one because the JSON family shares a printer and separates its
        /// dialects by entry point (`print`/`printc`/`print5`), and YAML's
        /// document printer is `printWith`.
        ///
        /// These ARE the serializer's dispatch: `ast/serialize_options.zig`
        /// calls `@field(d.Lang.Printer, d.print_name)(writer, ast, options)`
        /// (and the `print_node_name` twin) for every entry, so a wrong name
        /// here is a compile error rather than a wrong output. There is no
        /// separate fragment name: `serializeFragmentWith` uses `print_name`
        /// for every dialect but fig, whose `printFragment` takes an explicit
        /// arm there for the reason documented on that function.
        print_name: [:0]const u8 = "print",
        print_node_name: [:0]const u8 = "printNode",

        /// The `--spec` strings this dialect accepts and what each selects.
        /// Empty for the eleven dialects with a single grammar. See
        /// `cli/parse_dispatch.zig`'s `resolveSpec`, whose behaviour a
        /// comptime assert beside it pins against the registry.
        specs: []const SpecName(L) = &.{},

        /// How this format spells itself inside a host document, or null when
        /// it has no embedded form (`Embed.InnerFormat` is REIFIED from
        /// exactly the entries where this is non-null). `embed.zig` builds
        /// every fence, frontmatter marker, `<script type>` and `<code class>`
        /// it writes — and every tag/MIME it accepts on read — out of these
        /// fields, so they are the spelling, not a description of it.
        embed: ?EmbedSpellings = null,
    };
}

/// Everything the generic splice engine needs to know about a format's
/// surface syntax, indexed by dialect.
///
/// Obtained as `Language.syntax(t)` rather than as a constant because
/// `comments.line` genuinely varies by dialect: strict JSON has no comment
/// syntax while JSONC and JSON5 do, and the splice is reparsed under whichever
/// dialect the editor is holding. Making the whole struct a function of `Type`
/// keeps that question in one place instead of scattering per-field `fn(Type)`
/// types across the struct, and is where a TOML 1.0/1.1 or YAML 1.1/1.2.2
/// *editing* divergence would land if one ever appears.
///
/// The cost is a runtime switch where there used to be a comptime constant,
/// and it stops there: every consumer is an `appendSlice` call or an argument
/// to `commentBlockStart`/`entryBlockStart`. Nothing downstream needs a
/// comptime value — no array lengths, no `++`, no switch prongs.
pub const Syntax = struct {
    // ==================
    // COMMENTS
    // ==================

    /// This format's comment scanner and markers. See `Comments`.
    comments: Comments,

    // ==================
    // ENTRIES
    // ==================

    /// The mapping key/value separator spliced by the generic flow-entry
    /// insert helpers (`insertFlowMapEntry`/`insertFlowEntry`) and by
    /// `writeMapValue`'s block-insert path — or null for a format that owns
    /// every one of those paths itself and so has no answer to give.
    ///
    /// This is the separator the GENERIC engine writes, which need not be the
    /// separator the format's printer writes: ZON's struct-field syntax is
    /// ` = `, dotenv/`.properties` print a bare `=` with no surrounding
    /// spaces, INI always pads it. See each `printer.zig`.
    ///
    /// The null is not "no separator" — it is "not the generic engine's
    /// question". plist's entries are a pair of sibling ELEMENTS with no
    /// separator to write, and its `renderEntry` spells the whole entry;
    /// `language.validate` requires a null here to come with one, which is
    /// what makes every consumer below unreachable, and `editor.Editor.kvSep`
    /// is where that is cashed in. A format with a flow form whose separator
    /// varies by object (fig) declares its default here and
    /// `flow_kv_sep_from_siblings` beside it.
    kv_sep: ?[]const u8,

    /// Whether the flow-entry insert copies the separator the container's
    /// FIRST entry uses — the bytes between that entry's key and value —
    /// rather than writing `kv_sep`. True for fig, whose flow objects are
    /// either `=`-mode or `:`-mode (JSON-embedded) and may not mix the two,
    /// so the right separator is whichever the object already uses. An
    /// empty flow mapping has no first entry and takes `kv_sep`.
    flow_kv_sep_from_siblings: bool = false,

    /// Bytes written inside the braces around a freshly created single
    /// member of an EMPTY flow mapping: `" "` for fig's `{ x = 1 }`, none
    /// for JSON's and YAML's tight `{x: 1}`.
    flow_map_pad: []const u8 = "",

    /// How a logical key renders into this format's key syntax. See `KeyStyle`.
    key_style: KeyStyle = .verbatim,

    /// A sigil each key carries in the source that its AST key span EXCLUDES
    /// — ZON's leading `.`, whose span starts at the bare identifier.
    ///
    /// A flow-mapping entry delete backs up over it so the splice carries
    /// `.name` as a unit rather than stranding a bare `.` next to a survivor.
    key_sigil: ?u8 = null,

    // ==================
    // SHAPES
    // ==================

    /// The empty-mapping seed `set` splices to auto-vivify a missing ancestor,
    /// or null for a format that cannot vivify at all.
    ///
    /// Three distinct answers, and the null is not a degenerate case:
    ///
    ///   * Most formats use the flow `{}` literal, which each accepts as an
    ///     empty mapping value (JSON object, TOML inline table, fig flow map).
    ///     ZON spells it `.{}`. The dotted-key formats (fig/TOML) deliberately
    ///     keep `{}`: there the flow chain is the idiomatic intermediate form,
    ///     and `fig fmt` canonicalizes `a = { b = { c = v }}` to `a.b.c = v`.
    ///
    ///   * YAML seeds with NOTHING (`""`) — a bare `key:`, i.e. a null value.
    ///     Both spellings are valid YAML for "no entries yet" but they are not
    ///     interchangeable as a SEED: a flow `{}` can only ever be extended
    ///     with flow members, so every block-spelled value landing under a
    ///     vivified ancestor had to be refused (`BlockValueIntoFlow`). A null
    ///     value has the opposite property — `insertKey` promotes it to a real
    ///     block mapping (`promoteNullToMapping`), which takes block and inline
    ///     values alike — so `set(a.b.c, 1)` produces the block containers YAML
    ///     is normally written in.
    ///
    ///   * INI, plist and NestedText declare null: they have no literal
    ///     spelling for "an empty nested mapping" that the generic seed could
    ///     use. INI's case is the sharpest — `{}` there is a two-character
    ///     STRING value, not a container, so seeding with it would write a
    ///     nonsense `section = {}` root key. This is an ABSENCE of a syntax,
    ///     which is why it collapses into this field rather than standing as
    ///     a separate "can vivify" flag: `empty_map_literal` has exactly one
    ///     consumer, inside `set`'s vivify branch, so null and "excluded from
    ///     vivify" are the same statement.
    empty_map_literal: ?[]const u8,

    /// Whether a BLOCK (non-flow) sequence can be edited in place.
    ///
    /// False for TOML alone: a non-flow TOML sequence is an array-of-tables,
    /// which `appendContainerToSeq` handles instead, and TOML has no block
    /// scalar array. Append/prepend/remove/reorder all refuse with
    /// `NotAnInlineArray` when this is false.
    block_seq_editable: bool = true,

    /// Whether this format has FLOW container syntax at all — a `{…}` or
    /// `[…]` collection spelled inline, which the engine edits by comma-aware
    /// splice rather than by line.
    ///
    /// The engine tells flow from block by sniffing a container's first byte
    /// (`splice.isFlow`), and the sniff is right for every format that has
    /// both shapes. It is wrong for a format that has neither: INI's root
    /// span is the whole file, so a file opening with `[section]` reads as a
    /// bracket-delimited flow root, and NestedText's `- item` lines are not
    /// a flow sequence however a value happens to begin. Declaring false
    /// here answers "block" before the sniff runs. Every format with a flow
    /// spelling keeps the default; INI, NestedText, plist, dotenv and
    /// `.properties` declare false. See `editor.Editor.isFlowNode`, which
    /// also settles the one case a sniff cannot — a SECTION format's root,
    /// whose first byte is the first header's `[` — from `section_noun`.
    flow_containers: bool = true,

    /// The bytes one level of block nesting adds to a line's prefix: two
    /// spaces for YAML, four for NestedText, one `> ` marker cell for fig.
    ///
    /// The engine writes a block value that descends under a new entry
    /// (`key:` and then the value's lines) by copying the entry's own line
    /// prefix and appending this. It used to add two spaces to a column
    /// count, which is YAML's answer and nobody else's; a runtime format
    /// states its own. Read by `editor.Editor.writeMapValue` and
    /// `promoteNullToMapping`.
    indent_unit: []const u8 = "  ",

    /// What introduces a block-sequence item, separator included: `- ` for
    /// YAML and NestedText, `* ` for fig, `""` for plist, whose item is a
    /// bare element. The engine writes a new item as the first item's line
    /// prefix (the bytes before its marker — see `Document.node_marker_spans`)
    /// followed by this. Used to be a `"- "` literal in the engine.
    seq_item_marker: []const u8 = "- ",

    /// The tokens that open and close a block container, for a format whose
    /// block containers close themselves — plist's `<dict>`/`</dict>` and
    /// `<array>`/`</array>` — or null for every line-structured format, where
    /// a block collection has no closing token at all.
    ///
    /// Two things read it. The engine expands an EMPTY container of such a
    /// format (`<dict/>`) into its multi-line form around the first entry or
    /// item it inserts, since the childless form has no line to splice
    /// after; a format declaring null refuses that insert with
    /// `EmptyInlineContainer`, having no spelling for it. And a same-line
    /// trailing comment on a container value follows the close here, where
    /// in a line-structured format the value span begins at its first child
    /// on a later line and the comment rides the KEY's line (`contents: #
    /// note`). See `editor.Editor.expandEmptyContainer` and
    /// `trailingCommentWindow`.
    closed_containers: ?ClosedContainers = null,

    /// Whether a single line of the form `k: v` is a block MAPPING entry
    /// rather than scalar text — the one value shape that cannot be told
    /// apart by sniffing, so it is settled by the language's own parser.
    ///
    /// True for YAML alone: it is the only editable format whose block mapping
    /// has a single-line spelling reaching these splice paths. The flat
    /// formats (dotenv/`.properties`/INI) route through the same code, and
    /// there a `k: v` value is genuinely just scalar text — so they must keep
    /// splicing it inline, and declare false.
    single_line_block_mapping: bool = false,

    /// Whether a bare `key: value` document form exists — a keyless top-level
    /// mapping, as in YAML and JSON5.
    ///
    /// False for ZON alone, which has no such form: a null value, root or
    /// nested, promotes in place to a flow `.{ key = value }` container built
    /// from `flow_map_open`/`flow_map_close`, so `promoteNullToMapping` needs
    /// no root-versus-descend distinction there.
    bare_document_mapping: bool = true,

    /// The flow-mapping delimiters, used when promoting a null in a format
    /// with no `bare_document_mapping`. ZON's opener is `.{`.
    flow_map_open: []const u8 = "{",
    flow_map_close: []const u8 = "}",

    /// Whether a line's prefix is STRUCTURAL rather than whitespace.
    ///
    /// True for fig alone. Its `#`-only comment lines need the same `>`
    /// marker-run prefix as the line they anchor above — comment depth is
    /// load-bearing for attachment (see `fig/DESIGN.md`, "Comments") — so the
    /// "indent" a new comment copies is the raw byte range from the line start
    /// to the node's span, not the leading whitespace. `firstNonSpace` would
    /// stop at the `>` and yield bare whitespace, dropping the markers
    /// entirely. `span.start` already sits just past that prefix for every fig
    /// node (see `TNode.span` in `fig/parser.zig`), so slicing back to the line
    /// start recovers it exactly. Every other language's prefix is pure
    /// whitespace, where `firstNonSpace` and `span.start` agree anyway.
    structural_indent: bool = false,

    /// Declares this a SECTION format — one whose logical containers are
    /// assembled from lines scattered through the source (a TOML `[table]`,
    /// an INI `[section]`, a fig block container) — and names what the format
    /// calls such a container. Null for every format whose containers are
    /// contiguous, which is what the default says.
    ///
    /// Three things hang off a non-null value, all in `editor.zig`:
    ///
    ///   * the parser is expected to fill `Document.node_regions` with the
    ///     header lines of every such container (see that field), which is
    ///     what the generic whole-container ops and the line-splice guards
    ///     read — nothing here can check that the parser does, so the two
    ///     are a pair by contract rather than by `validate`;
    ///   * the generic `deleteContainer`/`moveContainer`/`reorderContainers`
    ///     are live, and refuse at comptime for a null;
    ///   * the engine's refusals are spelled in this vocabulary
    ///     (`CannotDeleteTable` for `.table`, `CannotDeleteSection` for
    ///     `.section`, …), so a format's own words survive in its errors.
    ///
    /// A VALUE rather than a hook because the engine's rule is the same for
    /// all three — "a section node cannot be line-spliced; use the container
    /// op" — and only the noun in the error differs.
    section_noun: ?SectionNoun = null,

    /// How a header line that opens a container of its own is spelled, for
    /// a section format that has one — TOML's `[a.b]` and `[[a.b]]`. With
    /// it the engine's `insertContainer` and `appendContainerToSeq` are
    /// live: they render the path through `key_style`, frame it with these
    /// tokens, and splice the line past the parent's whole extent. Null for
    /// a section format whose containers are not opened by a header line
    /// the engine could write on its own (fig's headers are bare dotted
    /// paths that `set` already creates; INI cannot vivify), and those ops
    /// refuse at comptime there as before.
    section_header: ?SectionHeader = null,

    /// The key that MERGES another mapping's entries into this one — YAML's
    /// `<<` — or null for a format with no such key. With it declared, a key
    /// the document resolves through a merge but never spells out is
    /// INHERITED: `replaceValAtPath` shadows it with a local entry
    /// (copy-on-write) and `deleteKey` refuses it with `MergeOnlyKey`, since
    /// there is no syntax to un-inherit one. The merge's resolution is core
    /// (`AST.mergedChild`); this field says whether the format has it.
    merge_key: ?[]const u8 = null,
};

/// The five fragment renderers a format may declare — `Decls.renderers` in
/// `language.zig`, as an enum the engine can ask about. `Editor.hasRenderer`
/// answers for a compiled language from `@hasDecl` and for a runtime one
/// from the language's own `hasRenderer(t, which)`, since a vtable answers
/// with a null pointer rather than an absent declaration.
pub const Renderer = enum {
    value,
    entry,
    item,
    tail,
    key,

    /// The declaration name: `renderValue` for `.value`.
    pub fn declName(comptime self: Renderer) []const u8 {
        return switch (self) {
            .value => "renderValue",
            .entry => "renderEntry",
            .item => "renderItem",
            .tail => "renderTail",
            .key => "renderKey",
        };
    }
};

/// Everything a renderer is told, as one record: every renderer takes it,
/// and reads the fields its fragment needs. `indent` is the line prefix
/// the fragment's first line sits after, which a continuation line copies;
/// `key` the key as written (entry, tail, key); `value` the value text,
/// already through `renderValue` for every renderer but the value's own;
/// `literal` what fig's bare-literal rules make of it (value); `old_key`
/// the key being renamed, as the source spells it (key).
///
/// `parent_key` and `parent_tag` say where the fragment goes: the
/// CONTAINER it is written into — the mapping an entry joins, the sequence
/// an item joins, the container holding a value replaced or a key renamed
/// — by the NAME of the key it stands under (decoded, not spelled; empty
/// at the root and for a container that is itself a sequence item) and by
/// its tag as the node table spells one (`!dependency`, `!!map`; empty
/// for none). An XML list spells an item by the list's item element name,
/// which is exactly those two. Empty where the engine has no container in
/// hand; nothing is filled for one renderer and not another.
pub const RenderRequest = struct {
    indent: []const u8 = "",
    key: []const u8 = "",
    value: []const u8 = "",
    literal: Literal = .string,
    old_key: []const u8 = "",
    parent_key: []const u8 = "",
    parent_tag: []const u8 = "",
};

/// What fig's bare-literal rules make of the text a value renderer is
/// handed: `null`, `true`/`false`, a number (integer or float), a datetime
/// shape, or a string — the classification the `.fig` dialect gives a bare
/// token (`languages/fig/tokenizer.zig`'s `sniffBare`: `Yes` and `007` stay
/// strings), computed once by the engine over the text trimmed of
/// whitespace and passed to `renderValue`, so that no format restates the
/// rule and every format's `set` means the same thing by `42`. A datetime
/// is a string in the node table; here it is its own answer, since a
/// renderer spells it differently (plist's `<date>`). The tag names are the
/// spelling on the runtime vtable and the helper wire.
pub const Literal = enum {
    @"null",
    bool,
    int,
    float,
    datetime,
    string,

    /// The literal named `name`, or null.
    pub fn parse(name: []const u8) ?Literal {
        inline for (@typeInfo(Literal).@"enum".fields) |f| {
            if (eql(f.name, name)) return @field(Literal, f.name);
        }
        return null;
    }

    fn eql(a: []const u8, b: []const u8) bool {
        if (a.len != b.len) return false;
        for (a, b) |x, y| if (x != y) return false;
        return true;
    }
};

/// An opening and closing token pair. See `Syntax.closed_containers`.
pub const Delimiters = struct { open: []const u8, close: []const u8 };

/// The self-closing block container spellings of a format whose containers
/// have them. See `Syntax.closed_containers`.
pub const ClosedContainers = struct { map: Delimiters, seq: Delimiters };

/// What a section format calls its scattered container — the one word that
/// differs between the three formats' otherwise identical refusals. See
/// `Syntax.section_noun`.
pub const SectionNoun = enum {
    /// TOML: `NotATable`, `CannotDeleteTable`, `CannotReplaceTable`,
    /// `CannotMoveTable`, `CannotReorderTables`.
    table,
    /// INI: `NotAContainer`, `CannotDeleteSection`, `CannotReplaceSection`,
    /// `CannotMoveSection`, `CannotReorderSections`.
    section,
    /// fig: `NotAContainer`, `CannotDeleteContainer`,
    /// `CannotReplaceContainer`, `CannotMoveContainer`,
    /// `CannotReorderContainers`.
    container,
};