tabnas-csv 0.6.5

CSV (RFC 4180) grammar plugin for the tabnas parsing engine, over the jsonic base grammar
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
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226
1227
1228
1229
1230
1231
1232
1233
1234
1235
1236
1237
1238
1239
1240
1241
1242
1243
1244
1245
1246
1247
1248
1249
1250
1251
1252
1253
1254
1255
1256
1257
1258
1259
1260
1261
1262
1263
1264
1265
1266
1267
1268
1269
1270
1271
1272
1273
1274
1275
1276
1277
1278
1279
1280
1281
1282
1283
1284
1285
1286
1287
1288
1289
1290
1291
1292
1293
1294
1295
1296
1297
1298
1299
1300
1301
1302
1303
1304
1305
1306
1307
1308
1309
1310
1311
1312
1313
1314
1315
1316
1317
1318
1319
1320
1321
1322
1323
1324
1325
1326
1327
1328
1329
1330
1331
1332
1333
1334
1335
1336
1337
1338
1339
1340
1341
1342
1343
1344
1345
1346
1347
1348
1349
1350
1351
1352
1353
1354
1355
1356
1357
1358
1359
1360
1361
1362
1363
1364
1365
1366
1367
1368
1369
1370
1371
1372
1373
1374
1375
1376
1377
1378
1379
1380
1381
1382
1383
1384
1385
1386
1387
1388
1389
1390
1391
1392
1393
1394
1395
1396
1397
1398
1399
1400
1401
1402
1403
1404
1405
1406
1407
1408
1409
1410
1411
1412
1413
1414
1415
1416
1417
1418
1419
1420
1421
1422
1423
1424
1425
1426
1427
1428
1429
1430
1431
1432
1433
1434
1435
1436
1437
1438
1439
1440
1441
1442
1443
1444
1445
1446
1447
1448
1449
1450
1451
1452
1453
1454
1455
1456
1457
1458
1459
1460
1461
1462
1463
1464
1465
1466
1467
1468
1469
1470
1471
1472
1473
1474
1475
1476
1477
1478
1479
1480
1481
1482
1483
1484
1485
1486
1487
1488
1489
1490
1491
1492
1493
1494
1495
1496
1497
1498
1499
1500
1501
1502
1503
1504
1505
1506
1507
1508
1509
1510
1511
1512
1513
1514
1515
1516
1517
1518
1519
1520
1521
1522
1523
1524
1525
1526
1527
1528
1529
1530
1531
1532
1533
1534
1535
1536
1537
1538
1539
1540
1541
1542
1543
1544
1545
1546
1547
1548
1549
1550
1551
1552
1553
1554
1555
1556
1557
1558
1559
1560
1561
1562
1563
1564
1565
1566
1567
1568
1569
1570
1571
1572
1573
1574
1575
1576
1577
1578
1579
1580
1581
1582
1583
1584
1585
1586
1587
1588
1589
1590
1591
1592
1593
1594
1595
1596
1597
1598
1599
1600
1601
1602
1603
1604
1605
1606
1607
1608
1609
1610
1611
1612
1613
1614
1615
1616
1617
1618
1619
1620
1621
1622
1623
1624
1625
1626
1627
1628
1629
1630
1631
1632
1633
1634
1635
1636
1637
1638
1639
1640
1641
1642
1643
1644
1645
1646
1647
1648
1649
1650
1651
1652
1653
1654
1655
1656
1657
1658
1659
1660
1661
1662
1663
1664
1665
1666
1667
1668
1669
1670
1671
1672
1673
1674
1675
1676
1677
1678
// Copyright (c) 2021-2026 Richard Rodger, MIT License

// The engine's error carries a code, position, hint and a formatted
// report, so it is large by design and `Result<_, TabnasError>` trips
// clippy's `result_large_err`. The engine allows the lint at its own
// crate root for the same reason, and so does tabnas-jsonic; boxing here
// would make `parse` return a different shape from `Tabnas::parse` and
// from the other two ports.
#![allow(clippy::result_large_err)]

//! The CSV grammar plugin for the `tabnas` parsing engine.
//!
//! CSV text becomes an array of objects (one per row, keyed by the header
//! row) or an array of arrays, with RFC 4180 `""`-escaped quoting, custom
//! field and record separators, streaming, and a strict / non-strict mode.
//! In strict mode (the default) every field is raw text; in non-strict
//! mode a field body can hold embedded jsonic (`[1,2]`, `{x:1}`).
//!
//! The plugin is not standalone: it layers on the relaxed-JSON grammar of
//! [`tabnas_jsonic`] (the `val` / `map` / `list` / `pair` / `elem` rules)
//! and reuses the engine's lexer, comment handling and lifecycle hooks,
//! exactly as the canonical TypeScript plugin in `ts/src/csv.ts` does.
//!
//! ```
//! let value = tabnas_csv::parse("name,age\nAlice,30\nBob,25")?;
//! assert_eq!(
//!     value.to_json().to_string(),
//!     r#"[{"name":"Alice","age":"30"},{"name":"Bob","age":"25"}]"#
//! );
//! # Ok::<(), tabnas_csv::CsvError>(())
//! ```
//!
//! TypeScript is canonical: `ts/src/csv.ts` defines behaviour and option
//! defaults, and `csv-grammar.jsonic` at the repository root is the
//! grammar every runtime embeds. The shared fixtures in `test/spec/*.tsv`
//! are the parity contract across TypeScript, Go and Rust.

use std::cell::RefCell;
use std::fmt;
use std::rc::Rc;
use std::sync::{Arc, OnceLock};

use indexmap::IndexMap;
use serde::{Deserialize, Serialize};
use serde_json::json;
use tabnas::{
    ActionError, AltSpec, Context, GrammarSetting, GrammarSpec, ImperativeLexMatcher, Options,
    Plugin, PluginError, Rule, RuleSpec, Tabnas, Tin, Token, Value, TIN_BD, TIN_ST,
};

/// This crate's version. It MUST equal `ts/package.json` "version": the
/// release orchestrator rewrites both, and `tests/version_test.rs` fails
/// the build if they drift. Mirrors `VERSION` in `ts/src/csv.ts` and
/// `const VERSION` in `go/csv.go`.
pub const VERSION: &str = "0.6.5";

/// The README's Rust examples run as doctests, so a stale one fails the
/// gate rather than misleading the reader. Its `toml` and `bash` fences
/// are skipped; rustdoc runs only the `rust` ones.
#[cfg(doctest)]
#[doc = include_str!("../README.md")]
mod readme_examples {}

/// The error a failed parse produces, re-exported so callers need not
/// depend on the engine crate directly. Its `code` is the contract the
/// shared fixtures pin: `csv_extra_field`, `csv_missing_field`, and the
/// engine's own `unexpected` and `unterminated_string`.
pub use tabnas::TabnasError as CsvError;

// --- BEGIN EMBEDDED csv-grammar.jsonic ---
const GRAMMAR_TEXT: &str = r##"
# CSV Grammar Definition
# Parsed by a standard Jsonic instance and passed to jsonic.grammar()
# Function references (@ prefixed) are resolved against the refs map
#
# Token naming:
#   #LN - line ending (removed from per-instance IGNORE set)
#   #SP - whitespace  (removed from per-instance IGNORE set in strict mode)
#   #CA - comma / field separator
#   #ZZ - end of input
#   #VAL - token set: text, string, number, value literals
#
# Rules csv, newline, record, text are fully defined here.
# Rules list, elem, val are modified in code (strict mode defines from scratch;
# non-strict prepends to existing defaults to preserve JSON parsing).

{
  options: rule: { start: csv }
  options: lex: { emptyResult: [] }
  # Placeholders are {braces}: the engine's error injector (strinject in
  # @tabnas/parser) substitutes {name} from the details object passed to
  # token.bad(). A $name is left in the message verbatim.
  options: error: {
    csv_extra_field: 'unexpected extra field value: {fsrc}'
    csv_missing_field: 'missing field'
  }
  options: hint: {
    csv_extra_field: 'Row {row} has too many fields (the first of which is: {fsrc}). Only {len}\nfields per row are expected.'
    csv_missing_field: 'Row {row} has too few fields. {len} fields per row are expected.'
  }

  rule: csv: open: [
    { s: '#ZZ' }
    { s: '#LN' p: newline c: '@not-record-empty' }
    { p: record }
  ]

  rule: newline: open: [
    { s: '#LN #LN' r: newline }
    { s: '#LN' r: newline }
    { s: '#ZZ' }
    { r: record }
  ]
  rule: newline: close: [
    { s: '#LN #LN' r: newline g: end }
    { s: '#LN' r: newline g: end }
    { s: '#ZZ' g: end }
    { r: record }
  ]

  rule: record: open: [
    { p: list }
  ]
  rule: record: close: [
    { s: '#ZZ' g: end }
    { s: '#LN #ZZ' b: 1 g: end }
    { s: '#LN' r: '@record-close-next' g: end }
  ]

  rule: text: open: [
    { s: ['#VAL' '#SP'] b: 1 r: text n: { text: 1 } g: 'csv,space,follows' a: '@text-follows' }
    { s: ['#SP' '#VAL'] r: text n: { text: 1 } g: 'csv,space,leads' a: '@text-leads' }
    { s: ['#SP' '#CA #LN #ZZ'] b: 1 n: { text: 1 } g: 'csv,end' a: '@text-end' }
    { s: '#SP' n: { text: 1 } g: 'csv,space' a: '@text-space' p: '@text-space-push' }
    {}
  ]
}
"##;
// --- END EMBEDDED csv-grammar.jsonic ---

/// The grammar source every runtime embeds, verbatim: the text of
/// `csv-grammar.jsonic` at the repository root. Exposed so a test can hold
/// the embedded copy to the file on disk.
pub fn grammar_text() -> &'static str {
    GRAMMAR_TEXT
}

/// The name every csv plugin registers under, and so the namespace of its
/// plugin options (`parser.plugin_options("csv")`).
const PLUGIN_NAME: &str = "csv";

// ---------------------------------------------------------------------------
// Named function references.
//
// The grammar names every closure, and the closures are registered on the
// instance BEFORE the document is installed, because the document is what
// looks them up. The lifecycle names follow the engine's `@<rule>-<phase>`
// convention and are wired onto their rule automatically.
// ---------------------------------------------------------------------------

const CSV_BO: &str = "@csv-bo";
const CSV_AC: &str = "@csv-ac";
const RECORD_BC: &str = "@record-bc";
const TEXT_BC: &str = "@text-bc";
const TEXT_FOLLOWS: &str = "@text-follows";
const TEXT_LEADS: &str = "@text-leads";
const TEXT_END: &str = "@text-end";
const TEXT_SPACE: &str = "@text-space";
const NOT_RECORD_EMPTY: &str = "@not-record-empty";
const RECORD_CLOSE_NEXT: &str = "@record-close-next";
const TEXT_SPACE_PUSH: &str = "@text-space-push";
/// The `options.lex.match.stringcsv.make` reference: the RFC 4180 string
/// matcher factory.
const CSV_STRING: &str = "@csv-string";

/// The matcher's priority. Below the engine's own bands (fixed literals sit
/// at 2e6, quoted strings at 5e6), so a `""`-escaped field is cut before
/// the standard string matcher can see its opening quote. The same order
/// the TypeScript and Go plugins register.
const CSV_STRING_ORDER: f64 = 100_000.0;

// ---------------------------------------------------------------------------
// Options
// ---------------------------------------------------------------------------

/// What a streaming parse reports, in order: `Start`, one `Record` per
/// data row, then `End`. The counterpart of the TypeScript `stream(what,
/// record)` callback's `'start'`, `'record'` and `'end'` events.
#[derive(Debug, Clone, PartialEq)]
pub enum StreamEvent {
    /// The parse has begun.
    Start,
    /// One parsed record, shaped by `object` exactly as it would have been
    /// stored in the result.
    Record(Value),
    /// The parse has finished.
    End,
}

/// A streaming callback: records are handed to it as they are parsed and
/// are not stored in the result, which comes back empty.
#[derive(Clone)]
pub struct Stream(Arc<dyn Fn(StreamEvent) + Send + Sync>);

impl Stream {
    /// Wrap a callback.
    pub fn new(callback: impl Fn(StreamEvent) + Send + Sync + 'static) -> Self {
        Stream(Arc::new(callback))
    }

    fn send(&self, event: StreamEvent) {
        (self.0)(event);
    }
}

impl fmt::Debug for Stream {
    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
        formatter.write_str("Stream(<function>)")
    }
}

/// Field options. See [`CsvOptions`].
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(default)]
pub struct FieldOptions {
    /// The field separator, when it is not a comma. May be several
    /// characters (`"~~"`).
    pub separation: Option<String>,
    /// The key prefix for a field beyond the header's columns: the third
    /// field of a two-column document is `field~2`.
    pub nonameprefix: String,
    /// The value an empty field takes. Any JSON value; the default is the
    /// empty string.
    pub empty: serde_json::Value,
    /// Explicit column names, used in place of a header row.
    pub names: Option<Vec<String>>,
    /// Fail the parse with `csv_extra_field` or `csv_missing_field` when a
    /// row's field count differs from the header's (or from
    /// `names.len()`). Inert without a known field list.
    pub exact: bool,
}

impl Default for FieldOptions {
    fn default() -> Self {
        FieldOptions {
            separation: None,
            nonameprefix: "field~".to_string(),
            empty: serde_json::Value::String(String::new()),
            names: None,
            exact: false,
        }
    }
}

/// Record options. See [`CsvOptions`].
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(default)]
pub struct RecordOptions {
    /// The record separator characters, when they are not the default
    /// `\n` / `\r\n` / `\r`.
    pub separators: Option<String>,
    /// Keep an empty line as a record of empty fields, rather than
    /// skipping it.
    pub empty: bool,
}

/// String options. See [`CsvOptions`].
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(default)]
pub struct StringOptions {
    /// The quote character of a quoted field.
    pub quote: String,
    /// Whether to use the RFC 4180 `""`-escaping string matcher. `None`
    /// means on in strict mode and off in non-strict mode; `Some(false)`
    /// in strict mode leaves quoted fields to the jsonic string matcher
    /// with its backslash escapes.
    pub csv: Option<bool>,
}

impl Default for StringOptions {
    fn default() -> Self {
        StringOptions {
            quote: "\"".to_string(),
            csv: None,
        }
    }
}

/// The plugin options: the counterpart of the TypeScript `CsvOptions` and
/// `Csv.defaults`, and of the Go `Defaults` map. [`Default`] is the
/// documented default set.
///
/// The three-state options (`trim`, `comment`, `number`, `value`) are
/// `Option<bool>`: `None` is the TypeScript `null`, which means "off in
/// strict mode, on in non-strict mode".
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(default)]
pub struct CsvOptions {
    /// Drop the whitespace around an unquoted field.
    pub trim: Option<bool>,
    /// Lex `#` comments, so a comment line is no record and a trailing
    /// comment is not field text.
    pub comment: Option<bool>,
    /// Lex numbers, so `1e2` is the number `100` rather than text.
    pub number: Option<bool>,
    /// Lex the value keywords, so `true`, `false` and `null` are values
    /// rather than text.
    pub value: Option<bool>,
    /// The first row names the columns.
    pub header: bool,
    /// Each record is an object keyed by column name; `false` gives an
    /// array of field values.
    pub object: bool,
    /// Every field is raw text. `false` lets a field body hold embedded
    /// jsonic (`[1,2]`, `{x:1}`, a backslash-escaped string) and turns
    /// `trim`, `comment`, `number` and `value` on unless set.
    pub strict: bool,
    /// Field options.
    pub field: FieldOptions,
    /// Record options.
    pub record: RecordOptions,
    /// String options.
    pub string: StringOptions,
    /// Stream records to a callback instead of collecting them. A closure
    /// cannot travel in the plugin option bag, so it is carried here,
    /// outside serialization, and reaches the plugin through
    /// [`plugin_with`] and [`make_with`].
    #[serde(skip)]
    pub stream: Option<Stream>,
}

impl Default for CsvOptions {
    fn default() -> Self {
        CsvOptions {
            trim: None,
            comment: None,
            number: None,
            value: None,
            header: true,
            object: true,
            strict: true,
            field: FieldOptions::default(),
            record: RecordOptions::default(),
            string: StringOptions::default(),
            stream: None,
        }
    }
}

impl CsvOptions {
    /// Read options out of a plugin option bag, the form
    /// [`Tabnas::use_plugin`] merges and hands to the plugin. A missing key
    /// takes its default; an unknown key is ignored, as in the other two
    /// runtimes.
    pub fn from_value(value: &Value) -> Result<Self, String> {
        serde_json::from_value(value.to_json()).map_err(|error| format!("csv options: {error}"))
    }

    /// The options as a plugin option bag, the form [`Plugin::with_defaults`]
    /// and [`Tabnas::use_plugin`] take. The `stream` callback does not
    /// travel; see [`plugin_with`].
    pub fn to_value(&self) -> Value {
        let json = serde_json::to_value(self).expect("the option struct serializes");
        Value::from_json(&json)
    }
}

/// The settings the closures capture: the options normalised the way the
/// TypeScript plugin normalises them at the top of `Csv`.
#[derive(Clone)]
struct Settings {
    strict: bool,
    objres: bool,
    header: bool,
    trim: bool,
    record_empty: bool,
    empty: Value,
    nonameprefix: String,
    names: Option<Vec<String>>,
    exact: bool,
    stream: Option<Stream>,
}

// ---------------------------------------------------------------------------
// Node helpers
// ---------------------------------------------------------------------------

/// Assign a rule's node: the Rust spelling of TypeScript `r.node = v`.
///
/// A pushed or replaced rule SHARES its parent's node cell, so writing
/// through `rule.node.borrow_mut()` would overwrite the parent's node too.
/// Assigning installs a fresh cell instead. The cell is borrowed directly
/// only to mutate a container the rule genuinely shares: pushing a field
/// onto the enclosing record, or a record onto the document.
fn set_node(rule: &mut Rule, value: Value) {
    rule.node = Rc::new(RefCell::new(value));
}

fn list_push(node: &mut Value, value: Value) {
    match node {
        Value::Array(items) => Arc::make_mut(items).push(value),
        Value::ListRef(list) => Arc::make_mut(list).value.push(value),
        _ => {}
    }
}

fn items_of(node: &Value) -> Vec<Value> {
    match node {
        Value::Array(items) => items.as_ref().clone(),
        Value::ListRef(list) => list.value.clone(),
        _ => Vec::new(),
    }
}

fn flag(rule: &Rule, name: &str) -> bool {
    matches!(rule.u.get(name), Some(Value::Bool(true)))
}

fn counter(rule: &Rule, name: &str) -> i32 {
    rule.n.get(name).copied().unwrap_or(0)
}

/// JavaScript's `Number::toString` (ECMA-262 6.1.6.1.20), which is what
/// the canonical TypeScript `'' + value` spells, and so what a numeric cell
/// becomes as field text or as a column name.
///
/// Rust's own `f64` formatting differs from it in three ways that reach a
/// header key. It keeps the sign of negative zero (`-0`, where JavaScript
/// says `0`). It never switches to exponent form, where JavaScript does so
/// at `1e21` and at `1e-7`. And `format!("{n:.0}")` prints a large integral
/// float's exact binary value (`123456789012345683968`) rather than its
/// shortest round-tripping digits (`123456789012345680000`).
fn js_number_to_string(number: f64) -> String {
    if number.is_nan() {
        return "NaN".to_string();
    }
    // Catches -0.0 as well: JavaScript spells both zeros "0".
    if number == 0.0 {
        return "0".to_string();
    }
    if number < 0.0 {
        return format!("-{}", js_number_to_string(-number));
    }
    if number.is_infinite() {
        return "Infinity".to_string();
    }

    // The specification wants the shortest digit string `s` that round-trips
    // (length `k`), and `n`, the position of the decimal point relative to
    // it. Rust's `{:e}` yields digits of exactly that shortest length.
    let shortest = format!("{number:e}");
    let shortest_k = shortest
        .split_once('e')
        .map(|(mantissa, _)| mantissa.chars().filter(char::is_ascii_digit).count())
        .expect("a finite f64 always formats with an exponent");

    // Re-render to that same length to settle a tie. Where two digit
    // strings of length `k` are equally close to `number`, the
    // specification takes the one ending in an even digit; Rust's shortest
    // form does not, but its exactly-rounded fixed-precision form does.
    let exponential = format!("{:.*e}", shortest_k - 1, number);
    let (mantissa, exponent) = exponential
        .split_once('e')
        .expect("a finite f64 always formats with an exponent");
    // Rounding can leave trailing zeros (and, on a carry, one digit too
    // many); dropping them keeps `s` shortest, which is what `k` means.
    let digits = mantissa
        .chars()
        .filter(|digit| *digit != '.')
        .collect::<String>();
    let digits = digits.trim_end_matches('0');
    let digits = if digits.is_empty() { "0" } else { digits };
    let k = digits.len() as i32;
    let n = exponent
        .parse::<i32>()
        .expect("a formatted exponent is an integer")
        + 1;

    // The four cases of the specification, in its order. The range bounds
    // are `k <= n <= 21`, `0 < n <= 21` and `-6 < n <= 0`.
    if (k..=21).contains(&n) {
        // Integral, with n - k trailing zeros to restore.
        let mut text = digits.to_string();
        text.push_str(&"0".repeat((n - k) as usize));
        text
    } else if (1..=21).contains(&n) {
        let point = n as usize;
        format!("{}.{}", &digits[..point], &digits[point..])
    } else if (-5..=0).contains(&n) {
        format!("0.{}{}", "0".repeat(-n as usize), digits)
    } else {
        // Exponent form. `n - 1` is never 0 here, so the sign is never "+0".
        let sign = if n - 1 < 0 { '-' } else { '+' };
        let power = (n - 1).abs();
        if k == 1 {
            format!("{digits}e{sign}{power}")
        } else {
            format!("{}.{}e{sign}{power}", &digits[..1], &digits[1..])
        }
    }
}

/// A JavaScript `'' + value`: what the text actions concatenate. Text and
/// string tokens carry their text; a number or keyword (`number` /
/// `value` on) spells itself the way JavaScript does.
fn value_text(value: &Value) -> String {
    match value {
        Value::Undefined => String::new(),
        Value::Null => "null".to_string(),
        Value::Bool(flag) => flag.to_string(),
        Value::Number(number) => js_number_to_string(*number),
        Value::String(text) => text.clone(),
        Value::Text(text) => text.string.clone(),
        other => other.to_json().to_string(),
    }
}

/// A token's value as text (`r.o0.val`), or its source when it carries no
/// value at all.
fn token_text(token: &Token) -> String {
    if token.val.is_undefined() {
        token.src.as_str().to_string()
    } else {
        value_text(&token.val)
    }
}

/// A header cell as a column name: `obj[cell] = ...` applies
/// ToPropertyKey, which for anything but a symbol is ToString.
///
/// Called from the one place the canonical converts a cell, the object
/// shaping in [`record_before_close`], and never on the header row
/// itself, which is stored raw. A reading that takes no name from a cell
/// -- `object: false`, `header: false`, a header-only document, or a
/// `field.exact` mismatch, which is checked first -- never reaches this.
///
/// `None` means JavaScript cannot make a primitive of the value at all,
/// which is every OBJECT. A jsonic object is allocated with a null
/// prototype, so it inherits neither `toString` nor `Symbol.toPrimitive`
/// and the canonical runtime throws
/// `TypeError: Cannot convert object to primitive value` instead of
/// naming the column. This port cannot raise a JavaScript `TypeError`, so
/// it refuses the document; `DIVERGENCE.md` records that choice.
///
/// That holds for a PARSED object. An object supplied as an OPTION value
/// is the other case, and the canonical answers it differently: the
/// option merge rebuilds a plain source object onto `Object.prototype` on
/// the way into the bag, so `String` names the column `[object Object]`
/// there and nothing throws, even for one the caller made with
/// `Object.create(null)`.
///
/// `from_option` is which of the two this cell is, and the caller knows
/// because there is exactly one option that can put a value in a header
/// row: `field.empty`, dropped into a syntactically empty cell before any
/// rule runs, so `,a` under `{field: {empty: <value>}}` takes its first
/// column name from it. The flag is set where the options are READ rather
/// than guessed from the value, because a `Value::Object` is the same
/// type either way, and it travels INTO [`join_text`], so an object
/// nested in an option array is answered the same as one at the top.
///
/// Falling through to [`value_text`] is what made the refusal necessary
/// in the first place: its last-resort arm renders the value as JSON,
/// which named the column `{"x":1.0}` for `{x:1}` and `[1.0,2.0]` for
/// `[1,2]`, neither of which the canonical runtime produces for any
/// input.
fn key_text(value: &Value, from_option: bool) -> Option<String> {
    match value {
        Value::Undefined
        | Value::Null
        | Value::Bool(_)
        | Value::Number(_)
        | Value::String(_)
        | Value::Text(_) => Some(value_text(value)),
        Value::Array(items) => join_text(items, from_option),
        Value::Object(_) if from_option => Some("[object Object]".to_string()),
        // A PARSED object, and the two reference wrappers the engine keeps
        // for its own bookkeeping, have no ToString to apply.
        _ => None,
    }
}

/// Whether two values are the SAME container, rather than two containers
/// that compare equal. It is how a cell is recognised as the one
/// `field.empty` supplied: an option value is cloned into the cell, and a
/// clone of a container is a refcount bump on the same allocation, while
/// the parser allocates its own. Equality would not do, because a parsed
/// `{q:1}` equals an option `{q:1}` and only one of them may be named.
fn same_container(value: &Value, other: &Value) -> bool {
    match (value, other) {
        (Value::Object(left), Value::Object(right)) => Arc::ptr_eq(left, right),
        (Value::Array(left), Value::Array(right)) => Arc::ptr_eq(left, right),
        _ => false,
    }
}

/// `Array.prototype.toString`, which is `join(',')` with no separator
/// argument (ECMA-262 23.1.3.17 and 23.1.3.34): every element is
/// converted by the same rules, `null` and `undefined` become the empty
/// string, and a nested array joins recursively, so `[1,[2,3]]` flattens
/// to `1,2,3` and `[]` is the empty string.
///
/// A null ELEMENT is the empty string while a null CELL is `null`: the
/// empty string comes from `join`, not from ToString, so it applies only
/// inside an array.
///
/// The recursion needs no depth bound and no seen-set. A parsed value is
/// a TREE: the engine folds each finished rule's value into its parent
/// and never stores a reference to an ancestor, so no element can reach
/// its own array and the walk always terminates. Its depth is the
/// document's bracket nesting, which `tabnas-jsonic` already bounds at
/// 127 containers with the parse budget this plugin inherits.
///
/// An OPTION value does reach this, through `field.empty`, and it cannot
/// contain itself either, which is where the Go port differs.
/// `field.empty` is a `serde_json::Value` and `field.names` a
/// `Vec<String>`, both owned acyclic trees. The engine's own `Value`
/// shares containers behind an `Arc`, but building a cycle out of one
/// needs a second handle on a container while its refcount is still 1,
/// which the type does not allow. Go's option is an unconstrained `any`,
/// a caller can spell `a := make([]any, 1); a[0] = a`, and there the same
/// walk had to be bounded.
///
/// `from_option` travels from [`key_text`], so an object nested in an
/// option array is named `[object Object]` where a parsed one refuses.
fn join_text(items: &[Value], from_option: bool) -> Option<String> {
    let mut joined = String::new();
    for (index, item) in items.iter().enumerate() {
        if 0 < index {
            joined.push(',');
        }
        match item {
            Value::Null | Value::Undefined => {}
            other => joined.push_str(&key_text(other, from_option)?),
        }
    }
    Some(joined)
}

fn number(value: usize) -> Value {
    Value::Number(value as f64)
}

// ---------------------------------------------------------------------------
// The closures the grammar names
// ---------------------------------------------------------------------------

/// The text of the previous text rule's node: the accumulator a chained
/// text rule (`r: text`) extends.
fn prev_text(rule: &Rule) -> String {
    rule.prev_rule
        .as_ref()
        .map(|prev| value_text(&prev.node.borrow()))
        .unwrap_or_default()
}

/// `r.node = v.node = text`, where `v` is the rule itself on the first
/// text rule of a chain and the previous one afterwards.
fn assign_text(rule: &mut Rule, chained: bool, text: String) {
    let value = Value::String(text);
    if chained {
        if let Some(prev) = rule.prev_rule.as_ref() {
            *prev.node.borrow_mut() = value.clone();
        }
    }
    set_node(rule, value);
}

/// Text then space: `x ` continues a field, so keep the text and read on.
fn text_follows(rule: &mut Rule, _context: &mut Context) -> Result<(), ActionError> {
    let chained = counter(rule, "text") != 1;
    let head = if chained {
        prev_text(rule)
    } else {
        String::new()
    };
    let text = head + &rule.o0().map(token_text).unwrap_or_default();
    assign_text(rule, chained, text);
    Ok(())
}

/// Space then text: the space is field text unless it leads a trimmed
/// field.
fn text_leads(trim: bool) -> impl Fn(&mut Rule, &mut Context) -> Result<(), ActionError> {
    move |rule, _context| {
        let count = counter(rule, "text");
        let chained = count != 1;
        let head = if chained {
            prev_text(rule)
        } else {
            String::new()
        };
        let space = if 2 <= count || !trim {
            rule.o0().map(|token| token.src.as_str().to_string())
        } else {
            None
        };
        let text = head
            + &space.unwrap_or_default()
            + &rule
                .o1()
                .map(|token| token.src.as_str().to_string())
                .unwrap_or_default();
        assign_text(rule, chained, text);
        Ok(())
    }
}

/// Trailing space before a separator, line end or end of source: field
/// text unless trimmed.
fn text_end(trim: bool) -> impl Fn(&mut Rule, &mut Context) -> Result<(), ActionError> {
    move |rule, _context| {
        let chained = counter(rule, "text") != 1;
        let head = if chained {
            prev_text(rule)
        } else {
            String::new()
        };
        let space = if trim {
            None
        } else {
            rule.o0().map(|token| token.src.as_str().to_string())
        };
        assign_text(rule, chained, head + &space.unwrap_or_default());
        Ok(())
    }
}

/// A lone space: in strict mode it is field text (unless trimmed); in
/// non-strict mode the action does nothing and the alternate pushes `val`
/// to read an embedded value.
fn text_space(
    strict: bool,
    trim: bool,
) -> impl Fn(&mut Rule, &mut Context) -> Result<(), ActionError> {
    let end = text_end(trim);
    move |rule, context| {
        if strict {
            end(rule, context)
        } else {
            Ok(())
        }
    }
}

/// `text` before-close: hand the field text (or the embedded value a
/// pushed `val` produced) to the enclosing `val`. This is `r.parent.node =
/// ...` in the canonical grammar, and it writes into the parent's cell.
fn text_before_close(rule: &mut Rule, _context: &mut Context) -> Result<(), ActionError> {
    let value = if rule.child_node.is_undefined() {
        rule.node.borrow().clone()
    } else {
        rule.child_node.clone()
    };
    if let Some(parent) = rule.parent_node.as_ref() {
        *parent.borrow_mut() = value;
    }
    Ok(())
}

fn record_index(context: &Context) -> usize {
    match context.u.get("recordI") {
        Some(Value::Number(index)) => *index as usize,
        _ => 0,
    }
}

/// `record` before-close: the header row names the columns; every later
/// row becomes a record, checked against the field count under
/// `field.exact`, shaped by `object`, and either streamed or pushed onto
/// the document.
fn record_before_close(
    settings: Settings,
) -> impl Fn(
    &mut Rule,
    &mut Context,
    Option<&tabnas::RuleSnapshot>,
    Option<Token>,
) -> Result<Option<Token>, ActionError> {
    move |rule, context, _next, out| {
        let record_i = record_index(context);
        // The field list is held as the RAW cells, the way the canonical
        // holds `ctx.u.fields`, because a cell becomes a column NAME only
        // where an object record is built. `field.names` widens into the
        // same shape so the two sources stay interchangeable.
        // `fields_from_options` is the provenance of the WHOLE list: a
        // header row is parsed, `field.names` is an option. A parsed
        // header row can still hold an option value, because `field.empty`
        // is dropped into an empty cell before any rule runs, so the flag
        // is taken per cell below as well.
        let (fields, fields_from_options): (Option<Vec<Value>>, bool) =
            match context.u.get("fields") {
                Some(Value::Array(names)) => (Some(names.as_ref().clone()), false),
                _ => (
                    settings.names.as_ref().map(|names| {
                        names
                            .iter()
                            .map(|name| Value::String(name.clone()))
                            .collect()
                    }),
                    true,
                ),
            };

        if record_i == 0 && settings.header {
            // Kept exactly as parsed, which is what the canonical keeps:
            // `ctx.u.fields = r.child.node`. Nothing is converted here,
            // and an empty list is still a field list, as it is in
            // JavaScript where `[]` is truthy.
            context.u.insert(
                "fields".to_string(),
                Value::array(items_of(&rule.child_node)),
            );
        } else {
            let mut record = items_of(&rule.child_node);

            // The field-count check is independent of the result SHAPE: it
            // only needs a known field list (from the header row or
            // `field.names`), so it fires for arrays as well as objects.
            if let Some(fields) = fields.as_ref() {
                if settings.exact && record.len() != fields.len() {
                    let code = if record.len() > fields.len() {
                        "csv_extra_field"
                    } else {
                        "csv_missing_field"
                    };
                    // The messages interpolate {row}, {len} and {fsrc}, so
                    // the details are supplied. `row` counts from 1 and
                    // includes the header line, so it matches the line
                    // number a spreadsheet or editor reports.
                    let mut details = vec![
                        ("row".to_string(), number(record_i + 1)),
                        ("len".to_string(), number(fields.len())),
                    ];
                    if let Some(extra) = record.get(fields.len()) {
                        details.push(("fsrc".to_string(), extra.clone()));
                    }
                    let mut token = context.t0().cloned().unwrap_or_else(|| {
                        Token::new("#BD", TIN_BD, Value::Undefined, "", Default::default())
                    });
                    token.bad_with_details(code, details);
                    return Ok(Some(token));
                }
            }

            let filled = |cell: Option<&Value>| match cell {
                Some(value) if !value.is_undefined() => value.clone(),
                _ => settings.empty.clone(),
            };

            let shaped = if settings.objres {
                let mut object = IndexMap::new();
                let mut index = 0;
                if let Some(fields) = fields.as_ref() {
                    for (position, cell) in fields.iter().enumerate() {
                        // This is the ONE place a header cell becomes a
                        // column name, and the only place the canonical
                        // converts one: `obj[fields[fI]] = ...` puts the
                        // raw cell through ToPropertyKey, which for
                        // anything but a symbol is ToString.
                        //
                        // A PARSED object has no ToString at all, and
                        // the canonical runtime throws a TypeError here
                        // rather than naming the column. This port cannot
                        // raise one, so it refuses the document with the
                        // engine's inherited `unexpected` code instead of
                        // inventing a name; `DIVERGENCE.md` records that
                        // choice. Refusing at the header row instead was
                        // too early: it also refused an `object: false`
                        // parse and a header-only document, neither of
                        // which ever asks for a name, and both of which
                        // the canonical returns a value for.
                        //
                        // An object an OPTION supplied is named
                        // `[object Object]`, which is what the canonical
                        // does for it, and `from_option` is how the two
                        // are told apart. The type cannot tell them
                        // apart: both are `Value::Object`.
                        let from_option =
                            fields_from_options || same_container(cell, &settings.empty);
                        let Some(name) = key_text(cell, from_option) else {
                            let mut token = context.t0().cloned().unwrap_or_else(|| {
                                Token::new("#BD", TIN_BD, Value::Undefined, "", Default::default())
                            });
                            token.bad("unexpected");
                            return Ok(Some(token));
                        };
                        object.insert(name, filled(record.get(position)));
                    }
                    index = fields.len();
                }
                while index < record.len() {
                    object.insert(
                        format!("{}{index}", settings.nonameprefix),
                        filled(record.get(index)),
                    );
                    index += 1;
                }
                Value::object(object)
            } else {
                for cell in record.iter_mut() {
                    if cell.is_undefined() {
                        *cell = settings.empty.clone();
                    }
                }
                Value::array(record)
            };

            match settings.stream.as_ref() {
                Some(stream) => stream.send(StreamEvent::Record(shaped)),
                None => list_push(&mut rule.node.borrow_mut(), shaped),
            }
        }

        context
            .u
            .insert("recordI".to_string(), number(record_i + 1));
        Ok(out)
    }
}

/// Register every closure the grammar names, and the string matcher
/// factory. Before the documents, because the documents look them up.
fn register_refs(parser: &mut Tabnas, settings: &Settings, quote: &str) {
    let stream = settings.stream.clone();
    parser.state_action_ref(CSV_BO, move |rule, context| {
        context.u.insert("recordI".to_string(), number(0));
        if let Some(stream) = stream.as_ref() {
            stream.send(StreamEvent::Start);
        }
        set_node(rule, Value::array(Vec::new()));
        Ok(())
    });

    let stream = settings.stream.clone();
    parser.state_action_ref(CSV_AC, move |_rule, _context| {
        if let Some(stream) = stream.as_ref() {
            stream.send(StreamEvent::End);
        }
        Ok(())
    });

    parser.state_action_with_next_ref(RECORD_BC, record_before_close(settings.clone()));
    parser.state_action_ref(TEXT_BC, text_before_close);

    parser.action_with_context(TEXT_FOLLOWS, text_follows);
    parser.action_with_context(TEXT_LEADS, text_leads(settings.trim));
    parser.action_with_context(TEXT_END, text_end(settings.trim));
    parser.action_with_context(TEXT_SPACE, text_space(settings.strict, settings.trim));

    let record_empty = settings.record_empty;
    parser.alt_condition(NOT_RECORD_EMPTY, move |_rule, _context| !record_empty);
    parser.alt_replace(RECORD_CLOSE_NEXT, move |_rule, _context| {
        Some(if record_empty { "record" } else { "newline" }.to_string())
    });

    let strict = settings.strict;
    parser.alt_push(TEXT_SPACE_PUSH, move |_rule, _context| {
        Some(if strict { "" } else { "val" }.to_string())
    });

    parser.lex_match_factory_ref(CSV_STRING, csv_string_matcher(quote));
}

// ---------------------------------------------------------------------------
// The RFC 4180 string matcher
// ---------------------------------------------------------------------------

/// The custom CSV string matcher factory: `"a""b"` is the text `a"b`.
///
/// A reduced copy of the standard jsonic string matcher, with the doubled
/// quote as the only escape. It is the counterpart of the TypeScript
/// `buildCsvStringMatcher` export and the Go `BuildCsvStringMatcher`, in
/// the shape the engine's `options.lex.match.<name>.make` reference takes:
/// register it with [`Tabnas::lex_match_factory_ref`] and name it from an
/// options document.
///
/// ```
/// use tabnas::GrammarSpec;
///
/// let mut parser = tabnas_jsonic::make();
/// parser.lex_match_factory_ref("@csv-string", tabnas_csv::csv_string_matcher("\""));
/// let spec = GrammarSpec::from_value(serde_json::json!({
///     "options": { "lex": { "match": {
///         "stringcsv": { "order": 100000, "make": "@csv-string" },
///     } } },
/// }))?;
/// parser.grammar(&spec)?;
/// assert_eq!(parser.parse(r#""a""b""#)?, tabnas::Value::String("a\"b".to_string()));
/// # Ok::<(), Box<dyn std::error::Error>>(())
/// ```
///
/// The matcher runs only where the engine wants a string token, and only
/// at the quote character. A line character inside the quotes is field
/// text (a quoted field may span lines). Any other control character is
/// field text too when the engine's `string.allowControl` option is on,
/// as the CSV plugin sets it, and `unprintable` when it is off, as it is
/// by default on a plain instance. A quote left open at the end of the
/// source is `unterminated_string`. Loop exhaustion is what detects the
/// open quote, so an odd number of quotes (`"""`, `"""""`) is caught
/// rather than read as a terminated string.
///
/// A quote that is not exactly one UTF-16 code unit leaves the matcher
/// inert, and the factory answers `None` rather than installing one. The
/// canonical opens a quoted field with
/// `quoteMap = { [options.string.quote]: true }` and tests
/// `quoteMap[src[sI]]`, where `src[sI]` is ONE UTF-16 code unit, so it
/// can only ever fire for a quote that is one code unit: the empty
/// string, a multi-character quote such as `ab`, and an astral character
/// such as U+1F600, which JavaScript holds as a surrogate PAIR, are all
/// inert there. Go carries the same guard, with the same reasoning.
pub fn csv_string_matcher(
    quote: impl Into<String>,
) -> impl Fn(&Options) -> Option<ImperativeLexMatcher> + Send + Sync + 'static {
    let quote: String = quote.into();
    let single_code_unit = {
        let mut characters = quote.chars();
        match (characters.next(), characters.next()) {
            (Some(character), None) => character.len_utf16() == 1,
            _ => false,
        }
    };
    move |options: &Options| {
        if !single_code_unit {
            return None;
        }
        let quote = quote.clone();
        let line_chars: Vec<char> = options.line.chars.chars().collect();
        let allow_control = options.string.allow_control;
        Some(Arc::new(
            move |lexer: &mut tabnas::Lexer<'_>, _rule: &mut Rule, _context: &mut Context| {
                let rest = lexer.remaining();
                if !rest.starts_with(quote.as_str()) {
                    return None;
                }
                let point = lexer.point();
                let mut text = String::new();
                let mut at = quote.len();
                let mut closed = false;

                while at < rest.len() {
                    let tail = &rest[at..];
                    if tail.starts_with(quote.as_str()) {
                        at += quote.len();
                        // A doubled quote is one literal quote; a single
                        // one closes the field.
                        if rest[at..].starts_with(quote.as_str()) {
                            text.push_str(&quote);
                            at += quote.len();
                        } else {
                            closed = true;
                            break;
                        }
                        continue;
                    }
                    let character = tail.chars().next().expect("inside the source");
                    if line_chars.contains(&character) {
                        text.push(character);
                    } else if (character as u32) < 32 && !allow_control {
                        // Any other control character is field text when
                        // the engine's `string.allowControl` is on, which
                        // the plugin turns on; only with it off is the
                        // character refused. The diagnostic points at the
                        // control character, as it does in TypeScript.
                        lexer.advance_chars(rest[..at].chars().count());
                        return Some(lexer.bad("unprintable"));
                    } else {
                        text.push(character);
                    }
                    at += character.len_utf8();
                }

                if !closed {
                    let start = point.site.pos;
                    let end = start + rest[..at].chars().count();
                    return Some(lexer.bad_span("unterminated_string", start, end));
                }

                let source = rest[..at].to_string();
                let token = lexer.token("#ST", TIN_ST, Value::String(text), source.as_str(), point);
                lexer.advance_chars(source.chars().count());
                Some(token)
            },
        ))
    }
}

// ---------------------------------------------------------------------------
// The documents
// ---------------------------------------------------------------------------

/// Whole numbers as integers. The grammar text is parsed by jsonic, whose
/// numbers are all `f64`, and the engine's document loader reads an
/// alternate's `b` and `n` as integers.
fn integral(value: &mut serde_json::Value) {
    match value {
        serde_json::Value::Number(number) => {
            if let Some(float) = number.as_f64() {
                if float.fract() == 0.0 && float.abs() < 9.0e15 {
                    if float < 0.0 {
                        *value = serde_json::Value::from(float as i64);
                    } else {
                        *value = serde_json::Value::from(float as u64);
                    }
                }
            }
        }
        serde_json::Value::Array(items) => items.iter_mut().for_each(integral),
        serde_json::Value::Object(entries) => entries.values_mut().for_each(integral),
        _ => {}
    }
}

/// The embedded grammar as a serialized document: parsed by a standard
/// jsonic instance, as the TypeScript `new Tabnas().use(jsonic).parse(
/// grammarText)` and the Go `parseGrammarText` do.
fn grammar_document() -> Result<serde_json::Value, PluginError> {
    let parsed = tabnas_jsonic::parse(GRAMMAR_TEXT).map_err(|error| {
        PluginError(format!("csv: the embedded grammar does not parse: {error}"))
    })?;
    let mut document = parsed.to_json();
    if !document.is_object() || document.get("rule").is_none() {
        return Err(PluginError(
            "csv: the embedded grammar has no `rule` table".to_string(),
        ));
    }
    integral(&mut document);
    Ok(document)
}

/// The per-instance engine options, mirroring `jsonicOptions` in
/// `ts/src/csv.ts`. The static options (`rule.start`, `lex.emptyResult`,
/// `error`, `hint`) live in the grammar text.
fn options_document(
    options: &CsvOptions,
    settings: &Settings,
    flags: &LexFlags,
) -> serde_json::Value {
    let strict = settings.strict;

    // Fixed-token overrides: in strict mode the JSON structural tokens and
    // the `:` key separator are switched off; a configured field separator
    // rebinds `#CA`.
    let mut fixed = serde_json::Map::new();
    if strict {
        for name in ["#OB", "#CB", "#OS", "#CS", "#CL"] {
            fixed.insert(name.to_string(), serde_json::Value::Null);
        }
    }
    if let Some(separation) = options
        .field
        .separation
        .as_deref()
        .filter(|separation| !separation.is_empty())
    {
        fixed.insert("#CA".to_string(), json!(separation));
    }

    // IGNORE set: `#LN` leaves it so row breaks are significant; in strict
    // mode `#SP` leaves it too so whitespace inside a field survives. This
    // engine merges a token set INDEX-WISE onto the one already installed,
    // as the canonical TypeScript deep merge does: a `null` drops that
    // position and a shorter array keeps the tail of the set it overlays.
    // So the override is spelled position for position over the jsonic
    // default `[#SP, #LN, #CM]`, exactly as `ts/src/csv.ts` spells it, and
    // the trailing position is named rather than left off: an override that
    // stops short of the installed length would keep whatever the tail
    // holds, and this one has to answer the same on a derived instance,
    // where the set it overlays is already the reduced one.
    let ignore = if strict {
        json!([null, null, "#CM"])
    } else {
        json!(["#SP", null, "#CM"])
    };

    let mut line = json!({ "single": settings.record_empty });
    if let Some(separators) = options.record.separators.as_deref() {
        line["chars"] = json!(separators);
        line["rowChars"] = json!(separators);
    }

    let mut document = json!({
        "options": {
            "rule": { "exclude": if strict { "jsonic,imp" } else { "imp" } },
            "fixed": { "token": fixed },
            "tokenSet": { "IGNORE": ignore },
            "number": { "lex": flags.number },
            "value": { "lex": flags.value },
            "comment": { "lex": flags.comment },
            // A quoted field holds every character but an unescaped quote,
            // the C0 controls included, as other CSV readers accept them:
            // the CSV renderer writes a cell holding a TAB, an ASCII 30 or
            // an ASCII 31 between quotes, because CSV has no other spelling
            // for it. The jsonic string matcher, which reads the quote
            // characters the CSV matcher does not own and every quote under
            // `string.csv: false` or in non-strict mode, refuses a raw
            // control as `unprintable` unless this is on, and
            // `csv_string_matcher` honours the same option.
            "string": { "allowControl": true },
            "line": line,
        },
    });
    if flags.csv_strings {
        document["options"]["lex"] = json!({
            "match": {
                "stringcsv": { "order": CSV_STRING_ORDER, "make": CSV_STRING },
            },
        });
    }
    document
}

/// The lexer switches the mode decides.
struct LexFlags {
    number: bool,
    value: bool,
    comment: bool,
    csv_strings: bool,
}

fn apply_document(parser: &mut Tabnas, document: serde_json::Value) -> Result<(), PluginError> {
    let spec = GrammarSpec::from_value(document).map_err(|error| PluginError(error.0))?;
    parser
        .grammar(&spec)
        .map_err(|error| PluginError(error.0))?;
    Ok(())
}

// ---------------------------------------------------------------------------
// The list / elem / val rules, modified in code
// ---------------------------------------------------------------------------

/// An alternate over token sequences, with the fields the CSV rules use.
fn alt(s: Vec<Vec<Tin>>, b: usize, g: &str) -> AltSpec {
    let mut alt = AltSpec::new();
    alt.s = s;
    alt.b = b;
    alt.g = g.to_string();
    alt
}

fn push_alt(name: &str) -> AltSpec {
    let mut alt = AltSpec::new();
    alt.p = Some(name.to_string());
    alt
}

fn with_push(mut alt: AltSpec, name: &str) -> AltSpec {
    alt.p = Some(name.to_string());
    alt
}

fn with_replace(mut alt: AltSpec, name: &str) -> AltSpec {
    alt.r = Some(name.to_string());
    alt
}

/// `rs.open([...])` / `rs.close([...])` with no `append`: the whole list
/// goes in front of the existing alternates, in its own order.
fn prepend_open(spec: &mut RuleSpec, alts: Vec<AltSpec>) {
    for alt in alts.into_iter().rev() {
        spec.prepend_open(alt);
    }
}

fn prepend_close(spec: &mut RuleSpec, alts: Vec<AltSpec>) {
    for alt in alts.into_iter().rev() {
        spec.prepend_close(alt);
    }
}

/// The tokens the CSV rules match.
struct Tokens {
    ln: Tin,
    ca: Tin,
    sp: Tin,
    zz: Tin,
    val: Vec<Tin>,
}

/// An empty element before a separator: `,` pushes the empty value and
/// marks the element done, so `elem` before-close does not push again.
fn empty_before_comma(empty: Value) -> impl Fn(&mut Rule, &mut Context) + Send + Sync + 'static {
    move |rule, _context| {
        list_push(&mut rule.node.borrow_mut(), empty.clone());
        rule.u_mut().insert("done".to_string(), Value::Bool(true));
    }
}

/// An empty element at the end of the line: `,\n` / `,<eof>`.
fn empty_at_end(empty: Value) -> impl Fn(&mut Rule, &mut Context) + Send + Sync + 'static {
    move |rule, _context| {
        list_push(&mut rule.node.borrow_mut(), empty.clone());
    }
}

/// Allocate the per-record field array. Under the engine's node model a
/// pushed child rule is seeded with the parent's node, and jsonic's
/// `list` before-open only allocates for an implicit list, which the CSV
/// `record -> list` path never is: without this the fields would land on
/// the inherited document array.
fn allocate_fields(rule: &mut Rule, _context: &mut Context) {
    set_node(rule, Value::array(Vec::new()));
}

/// Strict mode: `list`, `elem` and `val` are replaced entirely. The JSON
/// structural tokens are off, so only the CSV alternates are needed.
fn strict_rules(parser: &mut Tabnas, tokens: &Tokens, empty: &Value) {
    let Tokens {
        ln,
        ca,
        sp,
        zz,
        val,
    } = tokens;
    let (ln, ca, sp, zz) = (*ln, *ca, *sp, *zz);

    parser.define_rule("list", |spec| {
        spec.clear();
        spec.add_bo(allocate_fields);
        // Do not consume the LN that closes an empty record.
        spec.add_open(alt(vec![vec![ln]], 1, ""));
        spec.add_open(push_alt("elem"));
        // LN ends the record.
        spec.add_close(alt(vec![vec![ln]], 1, "end"));
        spec.add_close(alt(vec![vec![zz]], 0, "end"));
    });

    let before_comma = empty.clone();
    let at_end = empty.clone();
    parser.define_rule("elem", move |spec| {
        spec.clear();
        let mut comma = alt(vec![vec![ca]], 1, "");
        comma.add_action(empty_before_comma(before_comma));
        spec.add_open(comma);
        spec.add_open(push_alt("val"));

        let mut trailing = alt(vec![vec![ca], vec![ln, zz]], 1, "comma");
        trailing.add_action(empty_at_end(at_end));
        spec.add_close(trailing);
        // Next element.
        spec.add_close(with_replace(alt(vec![vec![ca]], 0, "comma"), "elem"));
        // LN ends the record.
        spec.add_close(alt(vec![vec![ln]], 1, "end"));
        spec.add_close(alt(vec![vec![zz]], 0, "end"));

        // Push the parsed field value, unless an open alternate already did.
        spec.add_bc(|rule, _context| {
            if !flag(rule, "done") && !rule.child_node.is_undefined() {
                let child = rule.child_node.clone();
                list_push(&mut rule.node.borrow_mut(), child);
            }
        });
    });

    let val = val.clone();
    parser.define_rule("val", move |spec| {
        spec.clear();
        // Reset the parent-seeded node so an empty value resolves to no
        // value, not to the inherited field array.
        spec.add_bo(|rule, _context| set_node(rule, Value::Undefined));
        // Text and space concatenation.
        spec.add_open(with_push(alt(vec![val.clone(), vec![sp]], 2, ""), "text"));
        spec.add_open(with_push(alt(vec![vec![sp]], 1, ""), "text"));
        // A plain value token.
        spec.add_open(alt(vec![val.clone()], 0, ""));
        // LN ends the record.
        spec.add_open(alt(vec![vec![ln]], 1, ""));
        // Coalesce: a child (text) node wins; else the matched scalar
        // token; else no value (an implicit empty field).
        spec.add_bc(|rule, context| {
            let unset = rule.node.borrow().is_undefined();
            if unset {
                if !rule.child_node.is_undefined() {
                    let child = rule.child_node.clone();
                    set_node(rule, child);
                } else if rule.os() != 0 {
                    let value = rule.resolve_open_value(0, context);
                    set_node(rule, value);
                }
            }
        });
    });
}

/// Non-strict mode: the CSV alternates go in FRONT of jsonic's, so the
/// default value alternates (handling `[1,2]`, `{x:1}` and the like) stay
/// available for an embedded value.
fn relaxed_rules(parser: &mut Tabnas, tokens: &Tokens, empty: &Value) {
    let Tokens {
        ln,
        ca,
        sp,
        zz,
        val,
    } = tokens;
    let (ln, ca, sp, zz) = (*ln, *ca, *sp, *zz);

    parser.define_rule("list", |spec| {
        spec.add_bo(allocate_fields);
        // Do not consume the LN that closes an empty record.
        prepend_open(spec, vec![alt(vec![vec![ln]], 1, "")]);
        // The elem-push fallback goes after the jsonic defaults.
        spec.add_open(push_alt("elem"));
        // LN ends the record; in front so these win over jsonic's close
        // alternates.
        prepend_close(
            spec,
            vec![alt(vec![vec![ln]], 1, "end"), alt(vec![vec![zz]], 0, "end")],
        );
    });

    let before_comma = empty.clone();
    let at_end = empty.clone();
    parser.define_rule("elem", move |spec| {
        let mut comma = alt(vec![vec![ca]], 1, "");
        comma.add_action(empty_before_comma(before_comma));
        prepend_open(spec, vec![comma]);

        let mut trailing = alt(vec![vec![ca], vec![ln, zz]], 1, "comma");
        trailing.add_action(empty_at_end(at_end));
        prepend_close(spec, vec![trailing, alt(vec![vec![ln]], 1, "end")]);
    });

    let val = val.clone();
    parser.define_rule("val", move |spec| {
        prepend_open(
            spec,
            vec![
                with_push(alt(vec![val.clone(), vec![sp]], 2, ""), "text"),
                with_push(alt(vec![vec![sp]], 1, ""), "text"),
                alt(vec![vec![ln]], 1, ""),
            ],
        );
    });
}

// ---------------------------------------------------------------------------
// Public surface
// ---------------------------------------------------------------------------

/// Whether the CSV grammar is already on this instance. A plugin re-run
/// (a derived instance rebuilds its plugins on a fresh rule set; an
/// instance that already carries the grammar must not gain a second copy
/// of every alternate) is judged by the one rule only csv installs.
fn grammar_installed(parser: &Tabnas) -> bool {
    parser.rule_names().iter().any(|name| name == "csv")
}

/// Install the CSV plugin on `parser`, which must already carry the jsonic
/// grammar: the counterpart of the TypeScript `Csv` plugin function and
/// the Go `Csv`.
///
/// ```
/// let mut parser = tabnas_jsonic::make();
/// tabnas_csv::csv(&mut parser, &tabnas_csv::CsvOptions::default())?;
/// let value = parser.parse("a,b\n1,2")?;
/// assert_eq!(value.to_json().to_string(), r#"[{"a":"1","b":"2"}]"#);
/// # Ok::<(), Box<dyn std::error::Error>>(())
/// ```
///
/// In order: the closures the grammar names are registered, the
/// per-instance engine options are applied, the embedded grammar is
/// installed with every alternate tagged `csv`, and the `list`, `elem` and
/// `val` rules are modified in code (replaced in strict mode, extended in
/// non-strict mode, so embedded jsonic values keep working).
pub fn csv(parser: &mut Tabnas, options: &CsvOptions) -> Result<(), PluginError> {
    if grammar_installed(parser) {
        return Ok(());
    }

    let strict = options.strict;

    // In strict mode jsonic value parsing is off and every field is raw
    // text. In non-strict mode the three-state options default to on.
    let (trim, comment, number, value) = if strict {
        (
            options.trim.unwrap_or(false),
            options.comment.unwrap_or(false),
            options.number.unwrap_or(false),
            options.value.unwrap_or(false),
        )
    } else {
        (
            options.trim.unwrap_or(true),
            options.comment.unwrap_or(true),
            options.number.unwrap_or(true),
            options.value.unwrap_or(true),
        )
    };
    let csv_strings = if strict {
        options.string.csv != Some(false)
    } else {
        options.string.csv == Some(true)
    };

    let settings = Settings {
        strict,
        objres: options.object,
        header: options.header,
        trim,
        record_empty: options.record.empty,
        empty: Value::from_json(&options.field.empty),
        nonameprefix: options.field.nonameprefix.clone(),
        names: options.field.names.clone(),
        exact: options.field.exact,
        stream: options.stream.clone(),
    };
    let flags = LexFlags {
        number,
        value,
        comment,
        csv_strings,
    };

    register_refs(parser, &settings, &options.string.quote);
    apply_document(parser, options_document(options, &settings, &flags))?;

    let spec =
        GrammarSpec::from_value(grammar_document()?).map_err(|error| PluginError(error.0))?;
    parser
        .grammar_with_setting(&spec, &GrammarSetting::groups("csv"))
        .map_err(|error| PluginError(format!("csv: failed to apply the grammar: {}", error.0)))?;

    // Usually [#TX, #ST, #NR, #VL].
    let tokens = Tokens {
        ln: parser.token("#LN"),
        ca: parser.token("#CA"),
        sp: parser.token("#SP"),
        zz: parser.token("#ZZ"),
        val: parser.token_set("VAL").unwrap_or_default(),
    };
    if strict {
        strict_rules(parser, &tokens, &settings.empty);
    } else {
        relaxed_rules(parser, &tokens, &settings.empty);
    }
    Ok(())
}

/// The plugin form of [`csv`], for [`Tabnas::use_plugin`], with the
/// documented defaults: the counterpart of `tn.use(Csv, options)` in
/// TypeScript and `j.UseDefaults(Csv, Defaults, options)` in Go. The
/// option bag `use_plugin` takes is deep-merged over the defaults and
/// read as a [`CsvOptions`].
///
/// ```
/// use tabnas::Value;
///
/// let mut parser = tabnas_jsonic::make();
/// let options = Value::from_json(&serde_json::json!({ "object": false }));
/// parser.use_plugin(tabnas_csv::plugin(), Some(options))?;
/// assert_eq!(parser.parse("a,b\n1,2")?.to_json().to_string(), r#"[["1","2"]]"#);
/// # Ok::<(), Box<dyn std::error::Error>>(())
/// ```
pub fn plugin() -> Plugin {
    Plugin::new(PLUGIN_NAME, |parser, options| {
        let options = CsvOptions::from_value(options).map_err(PluginError)?;
        csv(parser, &options)
    })
    .with_defaults(CsvOptions::default().to_value())
}

/// The plugin over typed options, including a [`Stream`] callback, which
/// cannot travel in the option bag. The bag `use_plugin` passes is
/// recorded on the instance as the plugin's options but the typed ones
/// are what the grammar is built from.
pub fn plugin_with(options: CsvOptions) -> Plugin {
    let defaults = options.to_value();
    Plugin::new(PLUGIN_NAME, move |parser, _bag| csv(parser, &options)).with_defaults(defaults)
}

/// Build a CSV parser with the given options: the counterpart of
/// `new Tabnas().use(jsonic).use(Csv, options)` and the Go
/// `jsonic.Make()` + `UseDefaults(Csv, Defaults, options)`.
///
/// Infallible by design: the jsonic base and the embedded grammar are
/// fixed, so a failure here is a bug in this crate rather than anything a
/// caller did.
///
/// ```
/// use tabnas_csv::CsvOptions;
///
/// let parser = tabnas_csv::make_with(CsvOptions { header: false, object: false, ..Default::default() });
/// assert_eq!(parser.parse("a,b\n1,2")?.to_json().to_string(), r#"[["a","b"],["1","2"]]"#);
/// # Ok::<(), tabnas_csv::CsvError>(())
/// ```
pub fn make_with(options: CsvOptions) -> Tabnas {
    let mut parser = tabnas_jsonic::make();
    parser
        .use_plugin(plugin_with(options), None)
        .expect("the csv grammar documents are fixed and valid");
    parser
}

/// Build a CSV parser with the default options.
///
/// ```
/// let parser = tabnas_csv::make();
/// assert_eq!(parser.parse("a\n\"b\"\"c\"")?.to_json().to_string(), r#"[{"a":"b\"c"}]"#);
/// assert_eq!(parser.parse("a\n\"b").unwrap_err().code, "unterminated_string");
/// # Ok::<(), tabnas_csv::CsvError>(())
/// ```
pub fn make() -> Tabnas {
    make_with(CsvOptions::default())
}

/// Parse a CSV source string with the shared default parser.
///
/// The engine is built once, on first use, and reused after that. Reuse
/// is safe: [`Tabnas::parse`] takes `&self` and builds a fresh parse
/// context per call, and `Tabnas` is `Send + Sync`, so concurrent callers
/// share one installed grammar instead of each rebuilding it, which is
/// what dominates a small parse.
///
/// Use [`make`] or [`make_with`] instead when the parser needs
/// configuring: that returns a fresh instance and leaves this one alone.
///
/// ```
/// let value = tabnas_csv::parse("a,b\n1,2\n3,4")?;
/// assert_eq!(value.to_json().to_string(), r#"[{"a":"1","b":"2"},{"a":"3","b":"4"}]"#);
/// # Ok::<(), tabnas_csv::CsvError>(())
/// ```
pub fn parse(src: &str) -> Result<Value, CsvError> {
    static DEFAULT: OnceLock<Tabnas> = OnceLock::new();
    DEFAULT.get_or_init(make).parse(src)
}

/// One optional alchemy source and the entry point a host calls.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct TranslationPart {
    /// The entry point a host calls.
    pub entry: &'static str,
    /// The source text, or `None` for an entry supplied by alchemy.
    pub source: Option<&'static str>,
}

/// The package-local structural translation interface.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct TranslationParts {
    /// The complete `tabnas.plugin.json` text.
    pub manifest: &'static str,
    /// An optional lift from the grammar's events to its first read shape.
    pub lift: Option<TranslationPart>,
    /// An optional render from the write shape to text.
    pub render: Option<TranslationPart>,
}

const TRANSLATION: TranslationParts = TranslationParts {
    manifest: include_str!("../translate/manifest.json"),
    lift: None,
    render: Some(TranslationPart {
        entry: "csv",
        source: None,
    }),
};

/// Return CSV's immutable translation parts.
#[must_use]
pub const fn translate() -> Option<TranslationParts> {
    Some(TRANSLATION)
}

/// The plugin's manifest, `tabnas.plugin.json`, as the repository carries
/// it. Its `translate` object is what a host that translates reads: the
/// shapes CSV is read as (`tree`, one object per record, keyed by the
/// header) and written from (`records`), the render that writes it, which
/// is the `csv` render alchemy carries rather than a file of this
/// repository's, and the sentences that say what that render changes. The
/// crate embeds its own copy, `translate/manifest.json`, since a packaged
/// crate holds nothing outside `rs/`; `tests/translate_test.rs` holds the
/// copy to the file.
///
/// ```
/// assert!(tabnas_csv::manifest_text().contains("\"translate\""));
/// ```
pub fn manifest_text() -> &'static str {
    TRANSLATION.manifest
}