fig-sys 3.0.3

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
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
const std = @import("std");
const Allocator = std.mem.Allocator;
const build_options = @import("build_options");
const manifest = @import("manifest.zig");

pub const Language = @This();

// The declared half of the interface, re-exported so a caller needs only this
// file. The definitions live in `manifest.zig` because it is a leaf — every
// `<lang>/<lang>.zig` imports it to spell its own `syntax`, and this file
// imports each of them in turn, so the types cannot live here without the
// manifest depending on the languages that declare it.
pub const CommentStyle = manifest.CommentStyle;
pub const KeyStyle = manifest.KeyStyle;
pub const Caps = manifest.Caps;
pub const Syntax = manifest.Syntax;

// Per-language gates: a compiled-out format resolves to `void`, so its module is
// never referenced and never built. Every call site that touches a gated
// `Language.*` must guard the access behind the same `build_options.lang_*`
// flag (a `comptime` check), or it will fail to compile against `void`. JSON is
// gateable like the rest now that `detect` no longer assumes it as a base.
pub const JSON = if (build_options.lang_json) @import("json/json.zig").Language else void;
pub const YAML = if (build_options.lang_yaml) @import("yaml/yaml.zig").Language else void;
pub const TOML = if (build_options.lang_toml) @import("toml/toml.zig").Language else void;
pub const ZON = if (build_options.lang_zon) @import("zon/zon.zig").Language else void;
pub const XML = if (build_options.lang_xml) @import("xml/xml.zig").Language else void;
pub const FIG = if (build_options.lang_fig) @import("fig/fig.zig").Language else void;
pub const INI = if (build_options.lang_ini) @import("ini/ini.zig").Language else void;
pub const DOTENV = if (build_options.lang_dotenv) @import("dotenv/dotenv.zig").Language else void;
pub const PROPERTIES = if (build_options.lang_properties) @import("properties/properties.zig").Language else void;
pub const PLIST = if (build_options.lang_plist) @import("plist/plist.zig").Language else void;
pub const NESTEDTEXT = if (build_options.lang_nestedtext) @import("nestedtext/nestedtext.zig").Language else void;

// ============================================================================
// THE FORMAT REGISTRY
// ============================================================================
//
// `compiled` (below) is the per-LANGUAGE list. This is the per-DIALECT one: the
// table the five hand-written parallel format enumerations — `Detected` here,
// `cli.Format`, `AST.SerializeFormat`, `c_api.FigFormat`, `deserialize.Format`,
// `Embed.InnerFormat` — are all restatements of, plus the per-dialect facts
// (ABI value, splice style, empty-document seed, `--spec` strings, embedded
// spellings) that today live scattered across six files as switches nothing
// cross-checks.
//
// As of this stage ALL SIX of those enums are REIFIED from it rather than
// merely pinned against it — `Detected` below, `cli.Format`,
// `AST.SerializeFormat`, `deserialize.Format`, `Embed.InnerFormat` and
// `c_api.FigFormat` are all `@Enum` over `namesOf(…)` — along with
// `cli/args.zig`'s extension table. The C ABI's enum is the one built over
// `abi_value` rather than over member positions, since its integers are a
// permanent contract; what remains hand-written beside it is a literal pin of
// those integers, plus `zig build abi-check` diffing the same values against
// fig.h. Every per-dialect FACT is now read rather than restated: the CLI's
// dispatch takes the splice style, empty-document seed and `--spec` strings,
// the serializer takes the printer names (`ast/serialize_options.zig`, whose
// three switches are one `inline else` each over
// `@field(d.Lang.Printer, d.print_name)`), and `embed.zig` takes the embedded
// spellings — its four literal builders and two tag/MIME resolvers are one
// `inline` over the registry each. `c_api.zig` reads the rest: `abi_value` for
// the enum, `Lang` for parser/capability/editor dispatch, and `dialect` for the
// three JSON ABI values that share one language.

/// `Lang.Type` when `Lang` is compiled in, `void` when it is gated out.
///
/// A `-D<lang>=false` build resolves that language to `void` above, 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 `Entry`, 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. Moved here from `cli/parse_dispatch.zig`, which now re-exports it —
/// the registry needs it one layer below the CLI.
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;
}

/// A named dialect of `Lang` spelled by its member NAME rather than by a
/// literal, so a registry entry can name one in a build where `Lang` is `void`
/// (there is no enum to write `.JSONC` against). Collapses to the `void` value
/// exactly when the language is gated out.
pub fn dial(comptime Lang: type, comptime tag: []const u8) DialectOf(Lang) {
    return if (Lang == void) {} else @field(Lang.Type, tag);
}

/// 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 today (and which `cli/edit_ops.zig` acts on), lifted
/// here so it is 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`, XML, plist, NestedText. (plist
    /// and NestedText *render* the text rather than splicing it; XML has no
    /// in-place editor at all, so no edit text ever reaches it.)
    raw,
};

/// One `--spec <version>` string and the dialect it selects. The element type
/// of `Entry.specs`, generic over the language so a gated-out one collapses to
/// a `void` dialect and the table 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: everything about it that is not the language
/// module itself. Generic over the language so the `void` protocol survives —
/// see `DialectOf`.
fn Entry(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.
        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 new entries append (which is why
        /// these run 1,2,7 down the JSON family — JSON5 arrived after XML).
        /// `zig build abi-check` compares these against fig.h's
        /// `FIG_FORMAT_*` enumerators in both directions.
        abi_value: c_int,

        /// Whether `detect` can sniff this dialect, i.e. whether it is a
        /// member of `Detected`. False for `jsonc` alone, which overlaps
        /// json/json5 on almost all input.
        detectable: bool = true,

        /// 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 this table.
        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: ?manifest.EmbedSpellings = null,
    };
}

/// EVERY user-facing dialect, as a heterogeneous comptime tuple — thirteen
/// entries over eleven languages (the JSON module supplies three).
///
/// Two properties of this table are frozen, and both are load-bearing:
///
///   * ORDER. It IS the member order of every enum derived from it
///     (`Detected`, `cli.Format`, `SerializeFormat`, `deserialize.Format`,
///     `Embed.InnerFormat` and `c_api.FigFormat` — the last of which takes its
///     member order from here but its VALUES from `abi_value`), and
///     reproduces the pre-registry `cli.Format` order minus its two
///     non-registry members (`canonical`, which is not a `Language` at all,
///     and `gron`, a CLI-only projection — both spliced back by hand at their
///     old positions, see `namesWith`) and minus `yml`, an alias of `yaml`
///     retired in Stage 3. Reordering it would silently renumber
///     `@intFromEnum` for every one of those enums. Append.
///
///   * `abi_value`. It is the C ABI, and a released value is permanent.
///
/// Entries are ALWAYS present — a gated-out language collapses its entry's
/// `Lang` to `void` rather than dropping the row — so every derived enum is
/// build-invariant and only the *behaviour* behind a member is gated.
///
/// `canonical` and `gron` are deliberately absent: canonical is the AST's own
/// oracle grammar (no `Language`, no dialect, an options-less printer) and
/// gron is a CLI-only projection of JSON. Both stay explicit named arms at
/// every switch, which is also what keeps an exhaustive switch honest — a new
/// member has to be either a registry entry or one of those two.
pub const dialects = .{
    Entry(JSON){
        .name = "json",
        .dialect = dial(JSON, "JSON"),
        .abi_value = 1,
        .deserializable = true,
        .splice = .json_string,
        .empty_doc_seed = "{}\n",
        .embed = .{
            .fence_tag = "json",
            .frontmatter = "---json",
            .script_mime = "application/json",
            .script_mime_aliases = &.{"application/ld+json"},
            .code_class = "language-json",
        },
    },
    Entry(JSON){
        .name = "jsonc",
        .dialect = dial(JSON, "JSONC"),
        .abi_value = 2,
        // The one non-detectable dialect: plain JSON and JSON5 already claim
        // everything JSONC accepts that they can parse, so sniffing it would
        // only ever mis-attribute a comment-free document.
        .detectable = false,
        .deserializable = true,
        .splice = .json_string,
        .empty_doc_seed = "{}\n",
        .print_name = "printc",
        .print_node_name = "printNodec",
    },
    Entry(JSON){
        .name = "json5",
        .dialect = dial(JSON, "JSON5"),
        // 7, not 6: JSON5 was added to the C ABI after XML, and a released
        // value is appended rather than inserted. Same for `fig` at 8 and the
        // five below it — the reason this enum's numbering is not its order.
        .abi_value = 7,
        .splice = .json_string,
        .empty_doc_seed = "{}\n",
        .print_name = "print5",
        .print_node_name = "printNode5",
    },
    Entry(YAML){
        .name = "yaml",
        .abi_value = 3,
        .deserializable = true,
        .splice = .literal,
        // A bare `key:` seed, not `{}`: see `Syntax.empty_map_literal`'s note
        // on why an empty YAML document is the empty string.
        .empty_doc_seed = "",
        .print_name = "printWith",
        .specs = &.{
            .{ .name = "1.2", .dialect = dial(YAML, "v1_2_2") },
            .{ .name = "1.2.2", .dialect = dial(YAML, "v1_2_2") },
            .{ .name = "1.1", .dialect = dial(YAML, "v1_1") },
            .{ .name = "1.1.0", .dialect = dial(YAML, "v1_1") },
        },
        .embed = .{
            .fence_tag = "yaml",
            .fence_aliases = &.{"yml"},
            // Bare, not `---yaml`: an untagged frontmatter block is YAML.
            .frontmatter = "---",
            .script_mime = "application/yaml",
            .script_mime_aliases = &.{ "application/x-yaml", "text/yaml" },
            .code_class = "language-yaml",
        },
    },
    Entry(TOML){
        .name = "toml",
        .abi_value = 4,
        .deserializable = true,
        .splice = .literal,
        .empty_doc_seed = "",
        .specs = &.{
            .{ .name = "1.0", .dialect = dial(TOML, "TOML_1_0") },
            .{ .name = "1.0.0", .dialect = dial(TOML, "TOML_1_0") },
            .{ .name = "1.1", .dialect = dial(TOML, "TOML_1_1") },
            .{ .name = "1.1.0", .dialect = dial(TOML, "TOML_1_1") },
        },
        .embed = .{
            .fence_tag = "toml",
            .frontmatter = "---toml",
            .script_mime = "application/toml",
            .code_class = "language-toml",
        },
    },
    Entry(ZON){
        .name = "zon",
        .abi_value = 5,
        .deserializable = true,
        .splice = .literal,
        .empty_doc_seed = ".{}\n",
    },
    Entry(XML){
        .name = "xml",
        .abi_value = 6,
        // XML has a reader and a writer but no in-place editor, so no edit
        // text ever reaches a splice; `.raw` is what `spliceStyle` says today.
        .splice = .raw,
        // No from-scratch creation: a bare XML document needs a root element
        // this layer cannot name.
        .empty_doc_seed = null,
    },
    Entry(FIG){
        .name = "fig",
        // The native authoring dialect (src/languages/fig/DESIGN.md): read,
        // written and edited by every surface.
        .abi_value = 8,
        .splice = .literal,
        .empty_doc_seed = "",
        .embed = .{
            .fence_tag = "fig",
            .fence_aliases = &.{"figl"},
            .frontmatter = "---fig",
            .script_mime = "application/figl",
            .script_mime_aliases = &.{"application/fig"},
            // `language-figl`, not `language-fig`: the class token and the
            // fence tag genuinely differ in `embed.zig` today.
            .code_class = "language-figl",
        },
    },
    Entry(INI){
        .name = "ini",
        // Untyped scalars: the grammar carries no type information, so
        // `port = 8080` reads back as the STRING "8080".
        .abi_value = 9,
        .splice = .raw,
        .empty_doc_seed = "",
    },
    Entry(DOTENV){
        .name = "dotenv",
        // A flat string map and nothing more: no nesting, untyped scalars. A
        // nested value tree cannot be represented, and serializing one warns.
        .abi_value = 10,
        .splice = .raw,
        .empty_doc_seed = "",
    },
    Entry(PROPERTIES){
        .name = "properties",
        // Flat and untyped, the same representational limits as dotenv.
        .abi_value = 11,
        .splice = .raw,
        .empty_doc_seed = "",
    },
    Entry(PLIST){
        .name = "plist",
        // Genuinely typed and nested (dict/array/string/integer/real/bool,
        // with date/data carried on the `extended` scalar) — the one XML-shaped
        // format here that is also a full value model.
        .abi_value = 12,
        .splice = .raw,
        // DELIBERATE DEVIATION from `cli/edit_ops.zig`'s `emptyDocSeed`, which
        // returns null for plist today — so `fig set` on a nonexistent
        // `.plist` refuses instead of creating one. A bare `<dict>` IS a
        // document `Language.PLIST` parses (see its `detect` probe), so the
        // registry declares the seed the fix needs. NOTHING READS IT YET: the
        // switch is converted in Stage 4, which is where the behaviour change
        // and its CLI test land. The assert beside `emptyDocSeed` exempts this
        // one row for exactly that reason.
        .empty_doc_seed = "<dict>\n</dict>\n",
    },
    Entry(NESTEDTEXT){
        .name = "nestedtext",
        // Nested (dict/list) but deliberately untyped — every leaf is a string.
        .abi_value = 13,
        .splice = .raw,
        .empty_doc_seed = "",
    },
};

/// The registry entry named `name`, or a compile error naming the format that
/// has none. The lookup every derived dispatch arm opens with.
pub fn entryFor(comptime name: []const u8) EntryOf(name) {
    inline for (dialects) |d| {
        if (comptime std.mem.eql(u8, d.name, name)) return d;
    }
    unreachable; // `EntryOf` already failed the build for an unknown name
}

/// The `Entry(L)` instantiation `entryFor(name)` returns — its own function
/// because each entry is a DIFFERENT type (they are generic over the language),
/// so the return type has to be computed from the name.
fn EntryOf(comptime name: []const u8) type {
    inline for (dialects) |d| {
        if (std.mem.eql(u8, d.name, name)) return @TypeOf(d);
    }
    @compileError("no registry entry for format '" ++ name ++ "'");
}

/// Which registry entries a derived enum is built from.
pub const Selector = enum {
    /// All thirteen.
    all,
    /// `.detectable` — `Language.Detected`.
    detectable,
    /// `.deserializable` — `deserialize.Format`.
    deserializable,
    /// `.embed != null` — `Embed.InnerFormat`.
    embeddable,
};

/// The names of the entries `sel` selects, in registry order. The expected
/// member list of the enum each selector names.
pub fn namesOf(comptime sel: Selector) []const [:0]const u8 {
    comptime {
        var out: []const [:0]const u8 = &.{};
        for (dialects) |d| {
            const take = switch (sel) {
                .all => true,
                .detectable => d.detectable,
                .deserializable => d.deserializable,
                .embeddable => d.embed != null,
            };
            if (take) out = out ++ [_][:0]const u8{d.name};
        }
        return out;
    }
}

/// A member a derived enum carries that the registry does not back: what it is
/// called, and which registry entry it sits immediately after. `cli.Format`'s
/// `canonical` (after `xml`) and `gron` (after `fig`), and `SerializeFormat`'s
/// `canonical`, are the only three uses — see `dialects`' note on why neither
/// format is an entry.
pub const Extra = struct {
    /// The registry entry name this member follows directly.
    after: []const u8,
    /// The member name. Sentinel-terminated: a reified enum's field
    /// names must be.
    name: [:0]const u8,
};

/// `namesOf(sel)` with each `extras` member spliced in directly after the
/// registry entry it names. The member list of a derived enum that has
/// hand-placed members on top of the registry's; the splice positions are what
/// reproduce the pre-reification member ORDER, so they are as load-bearing as
/// the registry's own order and are checked here rather than trusted.
pub fn namesWith(comptime sel: Selector, comptime extras: []const Extra) []const [:0]const u8 {
    comptime {
        @setEvalBranchQuota(20_000);
        var out: []const [:0]const u8 = &.{};
        var placed = [_]bool{false} ** extras.len;
        for (namesOf(sel)) |n| {
            out = out ++ [_][:0]const u8{n};
            for (extras, 0..) |x, xi| {
                if (!std.mem.eql(u8, x.after, n)) continue;
                if (placed[xi])
                    @compileError("two format-registry entries are named '" ++ x.after ++
                        "', so '" ++ x.name ++ "' has no single position to take");
                placed[xi] = true;
                out = out ++ [_][:0]const u8{x.name};
            }
        }
        for (extras, placed) |x, p| {
            if (!p)
                @compileError("the derived member '" ++ x.name ++ "' is placed after '" ++ x.after ++
                    "', which is not a format-registry entry this enum draws from");
        }
        return out;
    }
}

// Reifying an enum from a name list. This Zig spells type reification as
// granular builtins (`@Enum`/`@Union`) rather than `@Type(.{...})`; the
// in-tree precedent is `c_api.zig`'s `EditorUnion`.
//
// The two halves below are helpers rather than one `MakeEnum(names)` function
// deliberately: a type CREATED inside a generic function is named after that
// function, so every derived format enum would answer to
// `language.MakeEnum(&.{ &.{ ... }[0..(...)], … }[0..14])` in every compile
// error that mentions it. Calling `@Enum` at the declaration site instead
// gives each one its own name (`cli.types.Format`, `serialize_options
// .SerializeFormat`, …) — the error a missing switch arm produces is the whole
// point of these enums, so it is worth two call sites' worth of noise.

/// The tag type a registry-derived enum of `names.len` members gets:
/// `IntFittingRange(0, n - 1)` — exactly what Zig infers for a hand-written
/// `enum { … }` of the same size, so a reified enum is bit-for-bit the one it
/// replaces rather than merely name-compatible.
pub fn EnumTag(comptime member_names: []const [:0]const u8) type {
    if (member_names.len == 0)
        @compileError("a format enum derived from the registry must have at least one member");
    return std.math.IntFittingRange(0, member_names.len - 1);
}

/// The tag values of a registry-derived enum: 0..n-1 in `names` order, so the
/// member order IS the registry order and `@intFromEnum` keeps meaning what it
/// meant before reification. Pass as `&enumValues(names)`.
pub fn enumValues(comptime member_names: []const [:0]const u8) [member_names.len]EnumTag(member_names) {
    comptime {
        var values: [member_names.len]EnumTag(member_names) = undefined;
        for (&values, 0..) |*v, i| v.* = @intCast(i);
        return values;
    }
}

/// Fail the build unless `E`'s members are exactly `want` (in `want`'s order)
/// plus `extra` (which may sit anywhere, and must all be present). `what`
/// names the enum in the message.
///
/// The shape every "this enum is a restatement of the registry" assert needs:
/// order matters for the members that come FROM the registry, because that
/// order becomes theirs when the enum is reified, while the deliberate
/// non-registry members (`canonical`, `gron`) are positioned by hand and only
/// have to still exist.
///
/// Reification has since taken every caller — `cli.Format`,
/// `AST.SerializeFormat`, `deserialize.Format`, `Detected`,
/// `Embed.InnerFormat` and `c_api.FigFormat` are all BUILT from
/// `namesOf`/`namesWith` rather than checked against them, which is the
/// stronger statement. It stays for the next enum that must remain
/// hand-written and still restate the registry in order.
pub fn assertDerivedEnum(
    comptime E: type,
    comptime want: []const [:0]const u8,
    comptime extra: []const []const u8,
    comptime what: []const u8,
) void {
    comptime {
        @setEvalBranchQuota(20_000);
        var seen_extra = [_]bool{false} ** extra.len;
        var i: usize = 0;
        for (@typeInfo(E).@"enum".fields) |f| {
            var is_extra = false;
            for (extra, 0..) |x, xi| {
                if (std.mem.eql(u8, x, f.name)) {
                    seen_extra[xi] = true;
                    is_extra = true;
                }
            }
            if (is_extra) continue;
            if (i == want.len)
                @compileError(what ++ " has the member '" ++ f.name ++ "' after the last" ++
                    " registry entry — add it to `language.zig`'s `dialects`, or declare it" ++
                    " a deliberate non-registry member at this assert");
            if (!std.mem.eql(u8, want[i], f.name))
                @compileError(what ++ " member '" ++ f.name ++ "' sits where registry entry '" ++
                    want[i] ++ "' does — the registry's ORDER is the member order every" ++
                    " derived enum inherits, so the two cannot diverge");
            i += 1;
        }
        if (i != want.len)
            @compileError(what ++ " has no member for the registry entry '" ++ want[i] ++ "'");
        for (extra, seen_extra) |x, s| {
            if (!s) @compileError(what ++ " no longer has the non-registry member '" ++ x ++
                "' this assert exempts — drop it from the exemption list");
        }
    }
}

// The registry's self-consistency, plus the one derived enum that lives in this
// file. What each derived enum still states for itself — the `--spec` spellings
// beside `resolveSpec`, the ABI integers beside `c_api.FigFormat` — cannot be
// written here (this file sits BELOW all of them, and reaching up would invert
// the dependency), so each lives beside the enum it pins.
comptime {
    @setEvalBranchQuota(50_000);

    // Names and ABI values are both identities: a duplicate of either would
    // make a derived enum ill-formed (two members of one name) or the C ABI
    // ambiguous (two formats answering to one integer).
    for (namesOf(.all), 0..) |a, ai| {
        for (namesOf(.all)[ai + 1 ..]) |b| {
            if (std.mem.eql(u8, a, b))
                @compileError("two format-registry entries are both named '" ++ a ++ "'");
        }
    }
    for (dialects, 0..) |a, ai| {
        for (dialects, 0..) |b, bi| {
            if (bi > ai and a.abi_value == b.abi_value)
                @compileError("format-registry entries '" ++ a.name ++ "' and '" ++ b.name ++
                    "' share the ABI value " ++ std.fmt.comptimePrint("{d}", .{a.abi_value}) ++
                    " — released ABI values are permanent and unique");
        }
    }

    // Language ↔ registry bijection, both directions. A compiled-in language
    // with no entry would be a format the derived enums cannot name; an entry
    // whose (non-gated) language is not compiled in would be a member nothing
    // can serve.
    for (compiled) |Lang| {
        var found = false;
        for (dialects) |d| {
            if (d.Lang == Lang) found = true;
        }
        if (!found)
            @compileError("the compiled-in language '" ++ Lang.name ++
                "' has no entry in `dialects`, so no derived format enum can name it");
    }
    for (dialects) |d| {
        if (d.Lang == void) continue;
        var found = false;
        for (compiled) |Lang| {
            if (d.Lang == Lang) found = true;
        }
        if (!found)
            @compileError("format-registry entry '" ++ d.name ++
                "' names a language missing from `compiled`");
    }

    // Each entry's dialect. The JSON trio is the whole reason `dialect` is a
    // field rather than always `default_type`; everything else must BE the
    // language's default, which is what every current call site passes.
    for (dialects) |d| {
        if (d.Lang == void) continue;
        const expected = if (std.mem.eql(u8, d.name, "jsonc"))
            dial(d.Lang, "JSONC")
        else if (std.mem.eql(u8, d.name, "json5"))
            dial(d.Lang, "JSON5")
        else
            defaultDialect(d.Lang);
        if (d.dialect != expected)
            @compileError("format-registry entry '" ++ d.name ++
                "' selects a dialect other than the one its call sites pass today");
    }

    // `entryFor` itself: the lookup every derived dispatch in the CLI, the
    // serializer, `embed.zig` and the C ABI opens with, checked here so a
    // build that touches a format at all also proves the lookup works.
    if (entryFor("json").abi_value != 1 or entryFor("nestedtext").abi_value != 13)
        @compileError("`entryFor` does not return the entry it was asked for");

    // LAST, deliberately: `Detected` is the one derived enum living in this
    // file, so a registry that is internally inconsistent (a missing entry, a
    // duplicated name) would fail HERE too — with a message about `Detected`
    // rather than about the registry. Checking the table's own coherence first
    // means the error names the actual mistake.
    //
    // `Detected` is now REIFIED from `.detectable`, so "its members are the
    // detectable entries, in registry order" is true by construction and there
    // is nothing left to compare. What is still a real claim is which entries
    // carry the flag — the membership its doc comment argues for — so that is
    // what this checks: jsonc out, its json/json5 siblings in, and `canonical`
    // (no entry at all) absent.
    if (@hasField(Detected, "jsonc"))
        @compileError("the `jsonc` registry entry is marked detectable, but `detect` deliberately" ++
            " never sniffs it — it overlaps json/json5 on almost all input");
    if (!@hasField(Detected, "json") or !@hasField(Detected, "json5"))
        @compileError("`detect` returns `.json`/`.json5`, so both entries must stay detectable");
    if (@hasField(Detected, "canonical"))
        @compileError("`canonical` is not a registry entry and cannot be sniffed");
}

/// A format `detect` can recognize: the registry entries marked `detectable`,
/// in registry order. The `jsonc` dialect and `canonical` are deliberately
/// excluded: jsonc overlaps json/json5 on most input, and canonical is an
/// explicit selection rather than something to sniff (it is not a registry
/// entry at all). `fig` IS included, but slotted just ahead of YAML (see the
/// ordering note on `detect`) since its grammar overlaps TOML/YAML on plain
/// `key = value` content — it only wins detection on input that is either
/// invalid for every stricter format, or uses fig-only structural syntax (`>`
/// section depth, `*` elements, `+` continuations, `[]` group headers).
///
/// `detect` itself stays hand-written: the global ORDER the probes run in is a
/// single argument about grammar overlap that belongs in one place, and it is
/// not the member order — see the function below.
pub const Detected = @Enum(EnumTag(detected_names), .exhaustive, detected_names, &enumValues(detected_names));

const detected_names = namesOf(.detectable);

/// Best-effort content sniffing: try each COMPILED-IN parser and return the
/// first that accepts `input`, or null if none do (also what an
/// all-languages-disabled build returns). Order matters because the grammars
/// overlap — from most to least strict: JSON/JSON5, ZON, XML, TOML, then fig,
/// then INI, then YAML. fig sits just before INI/YAML, not after: YAML is so
/// permissive (a bare line is a valid plain scalar) that almost anything falls
/// through to it, which would starve fig (and INI) of a turn if it went last.
/// fig itself overlaps TOML heavily (both accept plain `key = value`), so it is
/// tried only after TOML has had first claim — a plain TOML-shaped document
/// still resolves to `.toml`, and fig only wins on content TOML can't parse
/// (its `>`/`*`/`+`/`[]` structural markers) or that is otherwise TOML-invalid.
/// INI overlaps TOML/fig too (same `[section]`/`key = value` shape) but accepts
/// strictly more — any raw, unquoted value text — so it sits right after fig
/// and wins only what both of those reject. dotenv sits last of the four
/// key/value-shaped formats since INI's grammar shadows almost all of it too
/// (see the `dotenv` branch below for the one thing that doesn't). This is a
/// heuristic, not a proof: input valid as more than one format resolves to
/// the earliest candidate in this order.
pub fn detect(allocator: Allocator, input: []const u8) ?Detected {
    if (comptime build_options.lang_json) {
        if (tryParse(JSON, allocator, input, .JSON)) return .json;
        if (tryParse(JSON, allocator, input, .JSON5)) return .json5;
    }
    if (comptime build_options.lang_zon) {
        if (tryParse(ZON, allocator, input, ZON.default_type)) return .zon;
    }
    if (comptime build_options.lang_plist) {
        // plist's DTD vocabulary (`<dict>`/`<array>`/`<key>`/...) is a STRICT
        // SUBSET of well-formed XML: the generic XML reader below would also
        // happily accept any real plist document, just folding it into a
        // differently-shaped AST (attribute/`#text` folding, no typed
        // scalars). So plist must get first claim, or a compiled-in XML
        // reader would starve it completely — the reverse isn't a problem:
        // plist's own grammar rejects anything outside its fixed element
        // vocabulary (`error.UnknownElement`), so ordinary XML falls through
        // to the `.xml` branch below untouched.
        if (tryParse(PLIST, allocator, input, PLIST.default_type)) return .plist;
    }
    if (comptime build_options.lang_xml) {
        if (tryParse(XML, allocator, input, XML.default_type)) return .xml;
    }
    if (comptime build_options.lang_toml) {
        if (tryParse(TOML, allocator, input, TOML.default_type)) return .toml;
    }
    if (comptime build_options.lang_fig) {
        if (tryParse(FIG, allocator, input, FIG.default_type)) return .fig;
    }
    if (comptime build_options.lang_ini) {
        // INI's grammar is also permissive (a bare `key = value` line, or an
        // empty file, both parse), so it's tried only after everything
        // stricter above has had first claim — it wins only on content those
        // reject, e.g. a `[section]` header or an unquoted value with
        // characters no TOML/fig scalar allows (`path = C:\a\b`).
        if (tryParse(INI, allocator, input, INI.default_type)) return .ini;
    }
    if (comptime build_options.lang_dotenv) {
        // dotenv is almost entirely shadowed by INI above: INI's key scanner
        // accepts any non-`=`/newline run (so even `export FOO=bar` parses as
        // one weird INI key) and its value decoding is quote-agnostic, so
        // nearly anything dotenv accepts, INI already claimed first. The one
        // thing only dotenv parses — a `"`/`'`-quoted value spanning a literal
        // embedded newline (INI's value never crosses a physical line) — is
        // this branch's actual reason to exist; `.env`'s real path to
        // selection is its extension (`detectLanguageFromFileEnding`
        // special-cases the `env` extension), not this content sniff.
        if (tryParse(DOTENV, allocator, input, DOTENV.default_type)) return .dotenv;
    }
    if (comptime build_options.lang_yaml) {
        if (tryParse(YAML, allocator, input, YAML.default_type)) return .yaml;
    }
    if (comptime build_options.lang_properties) {
        // `.properties` is even more permissive than YAML: a line with no
        // separator at all is still legal (a bare key, empty value — see
        // `properties/tokenizer.zig`), so nearly any UTF-8 text parses. It
        // therefore sits LAST, after YAML — the one thing this format
        // accepts that YAML rejects outright is a malformed-YAML shape
        // (see the test below); `.properties`'s real path to selection is
        // its extension, same as `.env`.
        if (tryParse(PROPERTIES, allocator, input, PROPERTIES.default_type)) return .properties;
    }
    if (comptime build_options.lang_nestedtext) {
        // NestedText goes LAST, after even `.properties` — not because its
        // own grammar is unusually permissive (it isn't: keys/values have
        // real restrictions, unlike `.properties`'s "nearly any text"), but
        // because a huge, ordinary swath of it — plain `key: value` lines
        // and `- item` lists — is ALSO valid YAML, and parses to a MEANINGFULLY
        // DIFFERENT tree there (YAML types `port: 80` as an integer;
        // NestedText's `port` is the untyped string `"80"`). Trying this
        // before YAML would silently change what today's `detect()` returns
        // for ordinary plain-YAML content already relied upon elsewhere in
        // this codebase — a real regression, not just an academic ambiguity
        // — so NestedText only gets a turn once every stricter-or-equally-
        // plausible format (including YAML) has already rejected the input.
        // Its real path to selection is the `.nt` extension (see
        // `cli/args.zig`), exactly like dotenv/`.properties` above.
        if (tryParse(NESTEDTEXT, allocator, input, NESTEDTEXT.default_type)) return .nestedtext;
    }
    return null;
}

/// Parse with `Lang` and report only whether it succeeded, releasing the document
/// either way. The detection probe — content is parsed, never retained.
fn tryParse(comptime Lang: type, allocator: Allocator, input: []const u8, t: Lang.Type) bool {
    const doc = Lang.Parser.parse(allocator, input, t) catch return false;
    doc.deinit(allocator);
    return true;
}

/// Every declaration a `Language` may carry. `validate` rejects anything not
/// named here, which is the whole point of the list: `@hasDecl` dispatch is
/// silent about names it does not recognize, so without a closed set an author
/// who writes `insertkey` gets a format that COMPILES, quietly runs the generic
/// implementation it meant to override, and corrupts a file on the first edit.
/// That was reproduced on the tree, not imagined — see the proposal's §10.5.
///
/// Adding a hook to `editor.zig` means adding its name here too. That is the
/// deliberate cost of the check, and the compiler charges it immediately: a
/// hook the list does not know is a hook no format can declare.
const Decls = struct {
    /// Required of every format, editable or not.
    ///
    /// `Printer` is the format's printer MODULE, and is distinct from the
    /// optional `printNode` decl below: the module is what the serializer
    /// dispatches through (`ast/serialize_options.zig` reaches
    /// `@field(d.Lang.Printer, d.print_name)` for every registry entry), so
    /// every format must expose one even when — as with plist and xml — its
    /// `Language` wraps only the module's `print`.
    const required = [_][]const u8{
        "Type",  "Parser", "Printer",    "default_type", "parse",
        "print", "name",   "extensions", "caps",
    };

    /// Required of an editable format only. `syntax` describes how the generic
    /// splice engine writes this format; asking a read-only format for one is
    /// asking it to describe an editing surface it does not have.
    const required_edit = [_][]const u8{"syntax"};

    /// Permitted, not required.
    ///
    ///   * `printNode` — every format but plist and xml, whose `print` is
    ///     written inline.
    ///   * `materialize`/`TagMode` — YAML only: collapsing the reference layer
    ///     before a non-YAML printer sees the tree. Callers already gate on
    ///     `@hasDecl(Lang, "materialize")`.
    const optional = [_][]const u8{ "printNode", "materialize", "TagMode" };

    /// Editing hooks. Declaring one takes over `editor.Editor`'s method of the
    /// same name — except `keyIsInherited` (a predicate the engine queries) and
    /// `seqItemLineStart` (a sub-computation), which are named for what they
    /// answer rather than for a method. Signatures are documented on the
    /// `Editor` method each overrides; see `editor.zig`.
    const hooks = [_][]const u8{
        "insertKey",         "deleteKeyGuard",
        "replaceValAtPath",  "replaceValAtPathFollowing",
        "replaceKeyAtPath",  "keyIsInherited",
        "seqItemLineStart",  "appendToSeq",
        "prependToSeq",      "removeSeqItem",
        "reorderSeqItems",   "addLeadingComment",
        "deleteLeadingComments", "getLeadingComment",
        "setTrailingComment", "deleteTrailingComment",
        "getTrailingComment",
    };

    /// Whole-container ops for SECTION formats — those whose logical containers
    /// are scattered through the source (TOML tables, fig block containers, INI
    /// sections). Unlike `hooks` these override nothing: the generic engine has
    /// no counterpart, because there is no single range to splice. Declaring one
    /// is still the whole of opting in; `editor.Editor`'s method of the same
    /// name dispatches on `@hasDecl` and refuses at comptime for a format that
    /// declares nothing (see its `requireSectionOp`).
    ///
    /// Kept as a separate set from `hooks` because the reachability rules below
    /// do not apply: a hook can be unreachable behind a `syntax` refusal, while
    /// one of these IS the operation and is reachable whenever it is declared.
    const exclusive = [_][]const u8{
        "deleteContainer",    "insertContainer",
        "renameContainer",    "moveContainer",
        "reorderContainers",  "appendContainerToSeq",
    };

    fn has(comptime set: []const []const u8, comptime name: []const u8) bool {
        for (set) |k| if (std.mem.eql(u8, k, name)) return true;
        return false;
    }

    fn known(comptime name: []const u8) bool {
        return has(&required, name) or has(&required_edit, name) or
            has(&optional, name) or has(&hooks, name) or has(&exclusive, name);
    }

    /// The known name `name` differs from only by letter case, or null.
    ///
    /// Not a general edit distance — deliberately. Every name above is
    /// camelCase, so the typo that actually costs something is a capitalization
    /// slip (`insertkey`, `appendtoseq`), and that is the one this catches. A
    /// wilder misspelling still fails; it just fails without a suggestion.
    fn nearest(comptime name: []const u8) ?[]const u8 {
        for ([_][]const []const u8{ &required, &required_edit, &optional, &hooks, &exclusive }) |set| {
            for (set) |k| if (std.ascii.eqlIgnoreCase(k, name)) return k;
        }
        return null;
    }
};

/// The enforcement point for the `Language` contract: every declaration a
/// format must supply, the closed set it may supply, plus the coherence rules
/// between them.
///
/// `Editor()` calls this for the format it is generic over, but that is not
/// enough on its own — a read-only format has no editor, so `validate(XML)`
/// would never be instantiated and XML's manifest would go unchecked. The
/// `comptime` block below this function closes that gap by running `validate`
/// over every compiled-in language, editable or not.
pub fn validate(comptime Lang: type) void {
    comptime {
        // Every check here is a linear scan over a name list, and the closed-set
        // check runs one such scan PER declaration — so the work is roughly
        // `decls × known-names` per format, times eleven formats from the
        // registry loop below. That clears the default 1000-branch budget
        // comfortably; the quota is per-evaluation, not a leak.
        @setEvalBranchQuota(20_000);

        // The original four, plus `Parser` — which `tryParse` and `Editor`
        // have both required in practice for as long as they have existed,
        // and which this now states — plus `Printer`, which the serializer's
        // registry-derived dispatch requires in exactly the same way.
        // `@typeName` rather than `Lang.name` here and in `required_edit`:
        // `name` is itself one of the declarations being checked, so it cannot
        // be relied on to identify the format that is missing it.
        for (Decls.required) |name| {
            if (!@hasDecl(Lang, name))
                @compileError(@typeName(Lang) ++ " must define " ++ name);
        }
        if (@TypeOf(Lang.caps) != Caps)
            @compileError("Language.caps must be a language.Caps");

        // `syntax` describes how the generic splice engine writes this
        // format, so it is required exactly when there is an editor to read
        // it. Requiring it unconditionally would be asking a read-only
        // format to describe an editing surface it does not have.
        if (Lang.caps.edit) {
            for (Decls.required_edit) |name| {
                if (!@hasDecl(Lang, name))
                    @compileError(@typeName(Lang) ++ " has caps.edit and must define " ++ name);
            }

            // Coherence: a format cannot have a same-line trailing comment
            // marker without having a comment syntax at all. Checked over
            // every dialect, since `syntax` is indexed by one.
            for (std.meta.tags(Lang.Type)) |t| {
                const s: Syntax = Lang.syntax(t);
                if (s.comments.trailing != null and s.comments.line == null)
                    @compileError("Language declares a trailing comment marker but no line comment marker");

                // Coherence: `kv_sep = null` says "the generic engine never
                // writes an entry for me". Every path that would is under the
                // generic `insertKey`, so the claim holds exactly when that op
                // is hooked — and `Editor.kvSep` refuses rather than
                // fabricating a separator if one is ever reached anyway.
                // Without this the null would be a silent `UnsupportedShape`
                // on an ordinary `set` instead of a compile error here.
                if (s.kv_sep == null and !@hasDecl(Lang, "insertKey"))
                    @compileError("Language declares kv_sep = null but does not hook insertKey," ++
                        " so the generic entry-insert paths have no separator to write");
            }
        }

        // The closed set. `@typeInfo(...).decls` lists only PUBLIC
        // declarations, so a format's private helpers — the
        // `const edit = @import("editor_helper.zig")` each hooks block opens
        // with — are invisible here and need no exemption.
        for (@typeInfo(Lang).@"struct".decls) |d| {
            if (Decls.known(d.name)) continue;
            @compileError("Language '" ++ Lang.name ++ "' declares unknown '" ++ d.name ++ "'" ++
                if (Decls.nearest(d.name)) |near|
                    " — did you mean '" ++ near ++ "'?"
                else
                    ". Editing hooks must be named for the `editor.Editor` method they" ++
                        " override, and added to `Decls.hooks` in language.zig" ++
                        " (or `Decls.exclusive` for a whole-container op).");
        }

        // Coherence: a format that says it cannot be edited must not declare
        // editing behaviour. Without this, `caps.edit = false` and a live hook
        // can disagree indefinitely — nothing else reads both. Whole-container
        // ops count: `Editor` is where they are reached, so declaring one on a
        // format with no editor is the same contradiction.
        if (!Lang.caps.edit) {
            for (Decls.hooks ++ Decls.exclusive) |name| {
                if (@hasDecl(Lang, name))
                    @compileError("Language '" ++ Lang.name ++ "' declares caps.edit = false" ++
                        " but supplies the editing hook '" ++ name ++ "'");
            }
            return;
        }

        // The remaining rules are about hooks being REACHABLE. Both follow from
        // where `editor.zig` dispatches, so both are dead-code checks rather
        // than taste: a hook the engine can never call is a silent no-op, and
        // silent is the failure mode this whole section exists to remove.

        // A block-sequence hook sits below `editor.zig`'s
        // `block_seq_editable` refusal, so a format that declares no editable
        // block sequences in any dialect can never reach one.
        var any_block_seq = false;
        var any_line_comment = false;
        for (std.meta.tags(Lang.Type)) |t| {
            const s: Syntax = Lang.syntax(t);
            if (s.block_seq_editable) any_block_seq = true;
            if (s.comments.line != null) any_line_comment = true;
        }
        if (!any_block_seq) {
            for ([_][]const u8{ "appendToSeq", "prependToSeq", "removeSeqItem", "reorderSeqItems" }) |name| {
                if (@hasDecl(Lang, name))
                    @compileError("Language '" ++ Lang.name ++ "' declares block_seq_editable = false" ++
                        " but supplies '" ++ name ++ "', which the engine refuses before reaching");
            }
        }

        // Comment hooks, the other direction. With no line-comment marker in
        // ANY dialect, every comment op is either hooked or permanently
        // `CommentsUnsupported` — so hooking SOME is almost certainly a
        // dropped delegation rather than a decision. plist is the case this
        // guards: `<!-- ... -->` is a delimiter pair with no leader, so it
        // declares null and hooks all six deliberately (see `plist.zig`), and
        // this makes losing one a compile error instead of a runtime refusal.
        //
        // Note this is the OPPOSITE of the rule the proposal's §4 proposed —
        // "trailing_comment == null alongside a declared setTrailingComment is
        // a contradiction". plist is exactly that pair, and is correct. A hook
        // does not read the marker, so a null marker beside a hook is not a
        // contradiction; it is the hook making the marker irrelevant.
        if (!any_line_comment) {
            const comment_hooks = [_][]const u8{
                "addLeadingComment",  "deleteLeadingComments", "getLeadingComment",
                "setTrailingComment", "deleteTrailingComment", "getTrailingComment",
            };
            var declared = 0;
            for (comment_hooks) |name| {
                if (@hasDecl(Lang, name)) declared += 1;
            }
            if (declared != 0 and declared != comment_hooks.len) {
                for (comment_hooks) |name| {
                    if (!@hasDecl(Lang, name))
                        @compileError("Language '" ++ Lang.name ++ "' has no line-comment marker" ++
                            " in any dialect and hooks some comment ops but not '" ++ name ++
                            "', which can then only ever return CommentsUnsupported");
                }
            }
        }
    }
}

/// Every compiled-in language, as a comptime list to iterate.
///
/// The set of formats written down ONCE, so anything that has to do something
/// per-format — `validate` below, the CLI's extension table — cannot fall out
/// of step with the set that actually exists. A gated-out format is ABSENT
/// here rather than present as `void`, so a consumer needs no gate of its own.
///
/// This is the "comptime registry with something to iterate" the proposal's §7
/// names as what the manifest unlocks. It does not by itself retire the five
/// parallel format enumerations — those are per-DIALECT and this is
/// per-LANGUAGE — but a consumer that is genuinely per-language now has one
/// list to walk instead of eleven `build_options` tests to repeat.
pub const compiled: []const type = blk: {
    var list: []const type = &.{};
    if (build_options.lang_json) list = list ++ [_]type{JSON};
    if (build_options.lang_yaml) list = list ++ [_]type{YAML};
    if (build_options.lang_toml) list = list ++ [_]type{TOML};
    if (build_options.lang_zon) list = list ++ [_]type{ZON};
    if (build_options.lang_xml) list = list ++ [_]type{XML};
    if (build_options.lang_fig) list = list ++ [_]type{FIG};
    if (build_options.lang_ini) list = list ++ [_]type{INI};
    if (build_options.lang_dotenv) list = list ++ [_]type{DOTENV};
    if (build_options.lang_properties) list = list ++ [_]type{PROPERTIES};
    if (build_options.lang_plist) list = list ++ [_]type{PLIST};
    if (build_options.lang_nestedtext) list = list ++ [_]type{NESTEDTEXT};
    break :blk list;
};

// Validate every compiled-in language, including the read-only ones that no
// `Editor()` instantiation would otherwise reach. Runs whenever this file is
// analyzed, which is whenever anything touches a format at all.
comptime {
    for (compiled) |Lang| validate(Lang);
}

test "detect identifies each compiled-in format by content" {
    const a = std.testing.allocator;
    if (comptime build_options.lang_json) {
        try std.testing.expectEqual(Detected.json, detect(a, "{\"x\":1}").?);
    }
    if (comptime build_options.lang_zon) {
        try std.testing.expectEqual(Detected.zon, detect(a, ".{ .x = 1 }").?);
    }
    if (comptime build_options.lang_plist) {
        try std.testing.expectEqual(Detected.plist, detect(a, "<dict><key>a</key><string>b</string></dict>").?);
    }
    if (comptime build_options.lang_xml) {
        try std.testing.expectEqual(Detected.xml, detect(a, "<r/>").?);
    }
    if (comptime build_options.lang_toml) {
        try std.testing.expectEqual(Detected.toml, detect(a, "x = 1\n").?);
    }
    if (comptime build_options.lang_fig) {
        // A bare container header line (no `=`, no `:`, no brackets) followed
        // by a `>`-depth child isn't valid JSON/ZON/XML/TOML, so this resolves
        // to fig even though it's tried before YAML.
        try std.testing.expectEqual(Detected.fig, detect(a, "database\n> host = localhost\n").?);
    }
    if (comptime build_options.lang_ini) {
        // A `;`-led comment line is invalid JSON/ZON/XML/TOML (TOML has no `;`
        // comment leader — its bare-key scanner rejects `;` outright) and not
        // fig syntax either, so this resolves to INI even though it's tried
        // right before YAML.
        try std.testing.expectEqual(Detected.ini, detect(a, "; header\nname = fig\n").?);
    }
    if (comptime build_options.lang_dotenv) {
        // A double-quoted value spanning a literal embedded newline is the one
        // shape only dotenv parses: INI's value never crosses a physical line
        // (it hits the line's `\n` first), so `[a]` on its own next line is a
        // bad INI statement — this falls all the way through INI to dotenv.
        try std.testing.expectEqual(Detected.dotenv, detect(a, "A=\"line1\nline2\"\n").?);
    }
    if (comptime build_options.lang_yaml) {
        // A plain mapping that is not valid JSON/TOML/fig/INI/etc. falls
        // through to YAML, the most permissive grammar and therefore tried
        // second-to-last.
        try std.testing.expectEqual(Detected.yaml, detect(a, "key: value\n").?);
    }
    if (comptime build_options.lang_properties) {
        // Malformed YAML (a scalar followed by unexpectedly-indented content)
        // still parses as `.properties`: worst case, each line is just a bare
        // key with an empty value (see `properties/tokenizer.zig`) — the most
        // permissive grammar of all, so it's tried dead last.
        try std.testing.expectEqual(Detected.properties, detect(a, "a: 1\n b: 2\n").?);
    }
}

test "detect: plain `key = value` prefers TOML over fig despite fig accepting it too" {
    const a = std.testing.allocator;
    if (comptime !build_options.lang_toml or !build_options.lang_fig) return error.SkipZigTest;
    // fig's root-level dotted assignment accepts the exact same shape TOML
    // does; TOML is tried first, so it wins the tie.
    try std.testing.expectEqual(Detected.toml, detect(a, "x = 1\n").?);
}

test "detect: a plist document prefers plist over generic xml despite xml accepting it too" {
    const a = std.testing.allocator;
    if (comptime !build_options.lang_plist or !build_options.lang_xml) return error.SkipZigTest;
    // Any well-formed plist is also well-formed generic XML; plist is tried
    // first, so it wins. Ordinary XML that isn't plist-shaped still falls
    // through to `.xml`.
    try std.testing.expectEqual(Detected.plist, detect(a, "<dict><key>a</key><string>b</string></dict>").?);
    try std.testing.expectEqual(Detected.xml, detect(a, "<r/>").?);
}