Skip to main content

tabnas/
grammar.rs

1// Copyright (c) 2013-2026 Richard Rodger, MIT License
2
3//! Loading and installing the JSON-serializable grammar interchange format.
4
5use crate::rule::{AltSpec, CompareOp, Condition, RuleSpec};
6use crate::utility::{modlist, ListMods};
7use crate::{
8    builtins::is_builtin_action, AltBack, AltBackWithMatch, AltCondition, AltConditionWithLexer,
9    AltConditionWithLexerAndMatch, AltConditionWithMatch, AltError, AltErrorWithMatch, AltModifier,
10    AltModifierWithMatch, AltNext, AltNextWithMatch, BudgetCheck, CommentSuffixMatcher,
11    ConfigModifier, ErrorSuffixCallback, ImperativeLexMatcher, LexCheck, LexMatcherCallback,
12    LexMatcherFactory, MapMerge, MatchTokenCallback, ParsePrepare, Tabnas, TextModifier, Value,
13    ValueTransform,
14};
15use indexmap::IndexMap;
16use regex::RegexBuilder;
17use serde_json::{Map, Value as JsonValue};
18use std::collections::HashMap;
19use std::fmt;
20
21/// Names a caller-keyed option map cannot carry, because the canonical
22/// TypeScript runtime refuses them: its deep merge skips `__proto__`,
23/// `constructor` and `prototype`, since merging one of those reaches the
24/// prototype chain and is prototype pollution. This port has no such
25/// hazard and could take them, but the code is the contract across
26/// runtimes, so a grammar that names a token set `constructor` fails
27/// here too rather than loading here and faulting there.
28const RESERVED_MAP_KEYS: [&str; 3] = ["__proto__", "constructor", "prototype"];
29
30#[derive(Clone)]
31struct AltRefs {
32    conditions: HashMap<String, AltCondition>,
33    match_conditions: HashMap<String, AltConditionWithMatch>,
34    lexer_conditions: HashMap<String, AltConditionWithLexer>,
35    lexer_match_conditions: HashMap<String, AltConditionWithLexerAndMatch>,
36    modifiers: HashMap<String, AltModifier>,
37    match_modifiers: HashMap<String, AltModifierWithMatch>,
38    errors: HashMap<String, AltError>,
39    match_errors: HashMap<String, AltErrorWithMatch>,
40    pushes: HashMap<String, AltNext>,
41    match_pushes: HashMap<String, AltNextWithMatch>,
42    replaces: HashMap<String, AltNext>,
43    match_replaces: HashMap<String, AltNextWithMatch>,
44    backtracks: HashMap<String, AltBack>,
45    match_backtracks: HashMap<String, AltBackWithMatch>,
46    match_tokens: HashMap<String, (MatchTokenCallback, bool)>,
47    value_transforms: HashMap<String, ValueTransform>,
48    text_modifiers: HashMap<String, TextModifier>,
49    lex_checks: HashMap<String, LexCheck>,
50    comment_suffixes: HashMap<String, CommentSuffixMatcher>,
51    match_values: HashMap<String, MatchTokenCallback>,
52    parse_prepares: HashMap<String, ParsePrepare>,
53    budget_checks: HashMap<String, BudgetCheck>,
54    lex_matches: HashMap<String, LexMatcherCallback>,
55    imperative_lex_matches: HashMap<String, ImperativeLexMatcher>,
56    lex_match_factories: HashMap<String, LexMatcherFactory>,
57    error_suffixes: HashMap<String, ErrorSuffixCallback>,
58    config_modifiers: HashMap<String, ConfigModifier>,
59    parser_starts: HashMap<String, crate::ParserStart>,
60    parser_starts_with_instance: HashMap<String, crate::ParserStartWithInstance>,
61    parser_starts_with_context: HashMap<String, crate::ParserStartWithContext>,
62    map_merges: HashMap<String, MapMerge>,
63}
64
65impl AltRefs {
66    /// Whether `name` is a registered reference of any kind.
67    fn knows(&self, name: &str) -> bool {
68        self.conditions.contains_key(name)
69            || self.match_conditions.contains_key(name)
70            || self.lexer_conditions.contains_key(name)
71            || self.lexer_match_conditions.contains_key(name)
72            || self.modifiers.contains_key(name)
73            || self.match_modifiers.contains_key(name)
74            || self.errors.contains_key(name)
75            || self.match_errors.contains_key(name)
76            || self.pushes.contains_key(name)
77            || self.match_pushes.contains_key(name)
78            || self.replaces.contains_key(name)
79            || self.match_replaces.contains_key(name)
80            || self.backtracks.contains_key(name)
81            || self.match_backtracks.contains_key(name)
82            || self.match_tokens.contains_key(name)
83            || self.value_transforms.contains_key(name)
84            || self.text_modifiers.contains_key(name)
85            || self.lex_checks.contains_key(name)
86            || self.comment_suffixes.contains_key(name)
87            || self.match_values.contains_key(name)
88            || self.parse_prepares.contains_key(name)
89            || self.budget_checks.contains_key(name)
90            || self.lex_matches.contains_key(name)
91            || self.imperative_lex_matches.contains_key(name)
92            || self.lex_match_factories.contains_key(name)
93            || self.error_suffixes.contains_key(name)
94            || self.config_modifiers.contains_key(name)
95            || self.parser_starts.contains_key(name)
96            || self.parser_starts_with_instance.contains_key(name)
97            || self.parser_starts_with_context.contains_key(name)
98    }
99}
100
101impl From<&Tabnas> for AltRefs {
102    fn from(tabnas: &Tabnas) -> Self {
103        Self {
104            conditions: tabnas.alt_conditions.clone(),
105            match_conditions: tabnas.alt_match_conditions.clone(),
106            lexer_conditions: tabnas.alt_lexer_conditions.clone(),
107            lexer_match_conditions: tabnas.alt_lexer_match_conditions.clone(),
108            modifiers: tabnas.alt_modifiers.clone(),
109            match_modifiers: tabnas.alt_match_modifiers.clone(),
110            errors: tabnas.alt_errors.clone(),
111            match_errors: tabnas.alt_match_errors.clone(),
112            pushes: tabnas.alt_pushes.clone(),
113            match_pushes: tabnas.alt_match_pushes.clone(),
114            replaces: tabnas.alt_replaces.clone(),
115            match_replaces: tabnas.alt_match_replaces.clone(),
116            backtracks: tabnas.alt_backtracks.clone(),
117            match_backtracks: tabnas.alt_match_backtracks.clone(),
118            match_tokens: tabnas.match_token_refs.clone(),
119            value_transforms: tabnas.value_transform_refs.clone(),
120            text_modifiers: tabnas.text_modifier_refs.clone(),
121            lex_checks: tabnas.lex_check_refs.clone(),
122            comment_suffixes: tabnas.comment_suffix_refs.clone(),
123            match_values: tabnas.match_value_refs.clone(),
124            parse_prepares: tabnas.parse_prepare_refs.clone(),
125            budget_checks: tabnas.budget_check_refs.clone(),
126            lex_matches: tabnas.lex_match_refs.clone(),
127            imperative_lex_matches: tabnas.imperative_lex_match_refs.clone(),
128            lex_match_factories: tabnas.lex_match_factory_refs.clone(),
129            error_suffixes: tabnas.error_suffix_refs.clone(),
130            config_modifiers: tabnas.config_modifier_refs.clone(),
131            parser_starts: tabnas.parser_start_refs.clone(),
132            parser_starts_with_instance: tabnas.parser_start_instance_refs.clone(),
133            parser_starts_with_context: tabnas.parser_start_context_refs.clone(),
134            map_merges: tabnas.map_merge_refs.clone(),
135        }
136    }
137}
138
139/// Builtin wire schema implemented by the Rust port. Schema v3 adds the
140/// `@fold$` tree action used by current BNF-family compiler output.
141/// Schema v4 adds `@key$` `lit`, the key as a constant rather than read
142/// from a token, for grammars that declare their shape instead of
143/// delimiting it. Schema v5 adds `src` to `@setval$`/`@push$`, taking a
144/// member's value from the source text the tree builders accumulated.
145pub const BUILTIN_SCHEMA_VERSION: u64 = 5;
146
147#[derive(Debug, Clone, PartialEq, Eq)]
148pub struct GrammarError(pub String);
149
150impl fmt::Display for GrammarError {
151    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
152        f.write_str(&self.0)
153    }
154}
155
156impl std::error::Error for GrammarError {}
157
158/// An ordered, immutable serialized grammar document.
159#[derive(Debug, Clone)]
160pub struct GrammarSpec {
161    document: JsonValue,
162    pub clear: bool,
163    pub version: Option<u64>,
164    pub meta: Option<IndexMap<String, JsonValue>>,
165}
166
167/// Group tags appended to every alternate while installing a grammar.
168#[derive(Debug, Clone, PartialEq, Eq)]
169pub enum GrammarGroups {
170    Csv(String),
171    List(Vec<String>),
172}
173
174impl From<&str> for GrammarGroups {
175    fn from(value: &str) -> Self {
176        Self::Csv(value.into())
177    }
178}
179
180impl From<String> for GrammarGroups {
181    fn from(value: String) -> Self {
182        Self::Csv(value)
183    }
184}
185
186impl From<Vec<String>> for GrammarGroups {
187    fn from(value: Vec<String>) -> Self {
188        Self::List(value)
189    }
190}
191
192impl GrammarGroups {
193    fn normalized(&self) -> Vec<String> {
194        match self {
195            Self::Csv(value) => value.split(',').map(str::to_owned).collect(),
196            Self::List(value) => value.clone(),
197        }
198        .into_iter()
199        .map(|value| value.trim().to_owned())
200        .filter(|value| !value.is_empty())
201        .collect()
202    }
203}
204
205#[derive(Debug, Clone, Default, PartialEq, Eq)]
206pub struct GrammarSettingAlt {
207    pub g: Option<GrammarGroups>,
208}
209
210#[derive(Debug, Clone, Default, PartialEq, Eq)]
211pub struct GrammarSettingRule {
212    pub alt: Option<GrammarSettingAlt>,
213}
214
215/// Optional settings applied across a grammar installation.
216#[derive(Debug, Clone, Default, PartialEq, Eq)]
217pub struct GrammarSetting {
218    pub rule: Option<GrammarSettingRule>,
219}
220
221impl GrammarSetting {
222    pub fn groups(groups: impl Into<GrammarGroups>) -> Self {
223        Self {
224            rule: Some(GrammarSettingRule {
225                alt: Some(GrammarSettingAlt {
226                    g: Some(groups.into()),
227                }),
228            }),
229        }
230    }
231
232    fn groups_ref(&self) -> Vec<String> {
233        self.rule
234            .as_ref()
235            .and_then(|rule| rule.alt.as_ref())
236            .and_then(|alt| alt.g.as_ref())
237            .map(GrammarGroups::normalized)
238            .unwrap_or_default()
239    }
240}
241
242impl GrammarSpec {
243    pub fn from_json(src: &str) -> Result<Self, GrammarError> {
244        let document: JsonValue = serde_json::from_str(src)
245            .map_err(|error| GrammarError(format!("Grammar: invalid JSON: {error}")))?;
246        Self::from_value(document)
247    }
248
249    pub fn from_slice(src: &[u8]) -> Result<Self, GrammarError> {
250        let document: JsonValue = serde_json::from_slice(src)
251            .map_err(|error| GrammarError(format!("Grammar: invalid JSON: {error}")))?;
252        Self::from_value(document)
253    }
254
255    pub fn from_value(document: JsonValue) -> Result<Self, GrammarError> {
256        let root = object(&document, "document")?;
257        let version = match root.get("v") {
258            None => None,
259            Some(JsonValue::Number(number)) => number.as_u64().filter(|value| *value > 0),
260            Some(_) => None,
261        };
262        if root.contains_key("v") && version.is_none() {
263            return Err(GrammarError(
264                "Grammar: invalid builtin schema version (expected a positive integer)".into(),
265            ));
266        }
267        if version.is_some_and(|value| value > BUILTIN_SCHEMA_VERSION) {
268            return Err(GrammarError(format!(
269                "Grammar: requires builtin schema version {}, but this engine supports up to {}",
270                version.unwrap_or_default(),
271                BUILTIN_SCHEMA_VERSION
272            )));
273        }
274        let meta = root.get("meta").and_then(JsonValue::as_object).map(|map| {
275            map.iter()
276                .map(|(key, value)| (key.clone(), value.clone()))
277                .collect()
278        });
279        Ok(Self {
280            clear: root.get("clear") == Some(&JsonValue::Bool(true)),
281            version,
282            meta,
283            document,
284        })
285    }
286}
287
288impl Tabnas {
289    /// Install a serialized grammar without mutating the caller's document.
290    /// Installation is transactional: an invalid option, rule, reference, or
291    /// builtin payload leaves the existing parser unchanged.
292    pub fn grammar(&mut self, grammar: &GrammarSpec) -> Result<&mut Self, GrammarError> {
293        self.grammar_with_setting(grammar, &GrammarSetting::default())
294    }
295
296    /// Install a serialized grammar while appending the setting's group tags
297    /// to every open and close alternate. The source grammar remains intact.
298    pub fn grammar_with_setting(
299        &mut self,
300        grammar: &GrammarSpec,
301        setting: &GrammarSetting,
302    ) -> Result<&mut Self, GrammarError> {
303        let mut staged = self.clone();
304        let mut grammar = grammar.clone();
305        append_setting_groups(&mut grammar.document, &setting.groups_ref())?;
306        staged.install_grammar(&grammar)?;
307        *self = staged;
308        Ok(self)
309    }
310
311    fn install_grammar(&mut self, grammar: &GrammarSpec) -> Result<(), GrammarError> {
312        let root = object(&grammar.document, "document")?;
313        let refs = AltRefs::from(&*self);
314        if self.options.config_modify.is_empty() {
315            // Native Rust callers can mutate the public typed tree directly.
316            // With no modifier-produced delta, it is also the authoritative
317            // raw option source for the next overlay.
318            self.raw_options = self.options.peek().clone();
319        }
320        if grammar.clear {
321            self.rules.clear();
322            self.options.fixed.tokens.clear();
323            self.raw_options.fixed.tokens.clear();
324        }
325        if let Some(options) = root.get("options") {
326            apply_options(&mut self.raw_options, object(options, "options")?, &refs)?;
327            let mut resolved = self.raw_options.clone();
328            resolved
329                .refresh_configuration()
330                .map_err(|error| GrammarError(format!("Grammar: {error}")))?;
331            *self.options = resolved;
332            self.plugin_options = self.options.plugin.clone();
333        }
334        if let Some(rules) = root.get("rule") {
335            for (name, value) in object(rules, "rule")? {
336                if value.is_null() {
337                    self.rules.shift_remove(name);
338                    continue;
339                }
340                let value = object(value, &format!("rule.{name}"))?;
341                let mut spec = self
342                    .rules
343                    .get(name)
344                    .cloned()
345                    .unwrap_or_else(|| RuleSpec::new(name));
346                if let Some(open) = value.get("open") {
347                    apply_alt_list(
348                        &mut spec.open,
349                        open,
350                        &format!("{name}.open"),
351                        &mut self.options,
352                        &refs,
353                    )?;
354                }
355                if let Some(close) = value.get("close") {
356                    apply_alt_list(
357                        &mut spec.close,
358                        close,
359                        &format!("{name}.close"),
360                        &mut self.options,
361                        &refs,
362                    )?;
363                }
364                wire_state_actions(self, &mut spec);
365                validate_action_references(self, &spec)?;
366                self.rules.insert(name.clone(), spec);
367            }
368        }
369        // Rule sequences may allocate tokens that were not named in the
370        // option document. They are instance identities, not modifier output,
371        // so carry them into the source tree for future configuration builds.
372        self.raw_options.tokens = self.options.tokens.clone();
373        self.emit_debug_config();
374        Ok(())
375    }
376
377    pub fn grammar_json(&mut self, src: &str) -> Result<&mut Self, GrammarError> {
378        let grammar = GrammarSpec::from_json(src)?;
379        self.grammar(&grammar)
380    }
381
382    pub fn grammar_json_with_setting(
383        &mut self,
384        src: &str,
385        setting: &GrammarSetting,
386    ) -> Result<&mut Self, GrammarError> {
387        let grammar = GrammarSpec::from_json(src)?;
388        self.grammar_with_setting(&grammar, setting)
389    }
390}
391
392fn append_setting_groups(document: &mut JsonValue, groups: &[String]) -> Result<(), GrammarError> {
393    if groups.is_empty() {
394        return Ok(());
395    }
396    let Some(rules) = document
397        .as_object_mut()
398        .and_then(|root| root.get_mut("rule"))
399        .and_then(JsonValue::as_object_mut)
400    else {
401        return Ok(());
402    };
403    for rule in rules.values_mut().filter_map(JsonValue::as_object_mut) {
404        for phase in ["open", "close"] {
405            let Some(value) = rule.get_mut(phase) else {
406                continue;
407            };
408            let alts = if value.is_array() {
409                value.as_array_mut()
410            } else {
411                value
412                    .as_object_mut()
413                    .and_then(|wrapper| wrapper.get_mut("alts"))
414                    .and_then(JsonValue::as_array_mut)
415            };
416            let Some(alts) = alts else {
417                continue;
418            };
419            for alt in alts.iter_mut().filter_map(JsonValue::as_object_mut) {
420                let mut existing = match alt.remove("g") {
421                    None | Some(JsonValue::Null) => Vec::new(),
422                    Some(JsonValue::String(value)) => value.split(',').map(str::to_owned).collect(),
423                    Some(JsonValue::Array(values)) => values
424                        .into_iter()
425                        .filter_map(|value| value.as_str().map(str::to_owned))
426                        .collect(),
427                    Some(_) => {
428                        return Err(GrammarError(
429                            "Grammar: alternate g must be a string or array".into(),
430                        ));
431                    }
432                };
433                existing.extend(groups.iter().cloned());
434                alt.insert(
435                    "g".into(),
436                    JsonValue::Array(existing.into_iter().map(JsonValue::String).collect()),
437                );
438            }
439        }
440    }
441    Ok(())
442}
443
444fn object<'a>(
445    value: &'a JsonValue,
446    label: &str,
447) -> Result<&'a Map<String, JsonValue>, GrammarError> {
448    value
449        .as_object()
450        .ok_or_else(|| GrammarError(format!("Grammar: {label} must be an object")))
451}
452
453fn apply_alt_list(
454    target: &mut Vec<AltSpec>,
455    value: &JsonValue,
456    label: &str,
457    options: &mut crate::Options,
458    refs: &AltRefs,
459) -> Result<(), GrammarError> {
460    let (alts, inject) = if let Some(array) = value.as_array() {
461        (array, None)
462    } else {
463        let wrapper = object(value, label)?;
464        let alts = wrapper
465            .get("alts")
466            .and_then(JsonValue::as_array)
467            .ok_or_else(|| GrammarError(format!("Grammar: {label}.alts must be an array")))?;
468        (alts, wrapper.get("inject").and_then(JsonValue::as_object))
469    };
470    if inject
471        .and_then(|item| item.get("clear"))
472        .and_then(JsonValue::as_bool)
473        == Some(true)
474    {
475        target.clear();
476    }
477    let mods = inject.map(|inject| ListMods {
478        delete: integer_list(inject.get("delete")),
479        move_items: integer_list(inject.get("move")),
480        custom: None,
481    });
482    *target = modlist(std::mem::take(target), mods.as_ref());
483    let parsed: Result<Vec<_>, _> = alts
484        .iter()
485        .enumerate()
486        .map(|(index, alt)| parse_alt(alt, &format!("{label} alt[{index}]"), options, refs))
487        .collect();
488    let mut parsed = parsed?;
489    if inject
490        .and_then(|item| item.get("append"))
491        .and_then(JsonValue::as_bool)
492        == Some(true)
493    {
494        target.append(&mut parsed);
495    } else {
496        parsed.append(target);
497        *target = parsed;
498    }
499    Ok(())
500}
501
502fn integer_list(value: Option<&JsonValue>) -> Vec<isize> {
503    value
504        .and_then(JsonValue::as_array)
505        .into_iter()
506        .flatten()
507        .filter_map(JsonValue::as_i64)
508        .filter_map(|value| isize::try_from(value).ok())
509        .collect()
510}
511
512fn parse_alt(
513    value: &JsonValue,
514    label: &str,
515    options: &mut crate::Options,
516    refs: &AltRefs,
517) -> Result<AltSpec, GrammarError> {
518    let map = object(value, label)?;
519    let mut alt = AltSpec::default();
520    if let Some(spec) = map.get("s") {
521        let slots: Vec<&str> = match spec {
522            JsonValue::String(sequence) => sequence.split_whitespace().collect(),
523            JsonValue::Array(sequence) => sequence
524                .iter()
525                .map(|slot| {
526                    slot.as_str().ok_or_else(|| {
527                        GrammarError(format!("Grammar: {label}.s entries must be strings"))
528                    })
529                })
530                .collect::<Result<_, _>>()?,
531            _ => {
532                return Err(GrammarError(format!(
533                    "Grammar: {label}.s must be a string or array"
534                )))
535            }
536        };
537        for slot in slots {
538            let mut tins = Vec::new();
539            let mut names = Vec::new();
540            for name in slot.split_whitespace() {
541                names.push(name.to_string());
542                if let Some(set) = options.token_set.get(name.trim_start_matches('#')) {
543                    tins.extend(set.iter().copied());
544                } else {
545                    tins.push(
546                        options
547                            .token(name)
548                            .unwrap_or_else(|| options.register_token(name)),
549                    );
550                }
551            }
552            // The names are kept so a token set overridden later still
553            // reaches this alternate, and what they resolved to beside
554            // them, so a slot set by hand afterwards is left alone: see
555            // `AltSpec::s_names` and `AltSpec::s_bound`.
556            alt.s_bound.push(tins.clone());
557            alt.s.push(tins);
558            alt.s_names.push(names);
559        }
560    }
561    match map.get("b") {
562        None | Some(JsonValue::Null) | Some(JsonValue::Bool(false)) => {}
563        Some(JsonValue::String(reference)) if reference.starts_with('@') => {
564            if let Some(backtrack) = refs.match_backtracks.get(reference) {
565                alt.b_match = Some(backtrack.clone());
566            } else {
567                alt.b_fn = Some(refs.backtracks.get(reference).cloned().ok_or_else(|| {
568                    GrammarError(format!(
569                        "Grammar: unknown backtrack function reference: {reference}"
570                    ))
571                })?);
572            }
573        }
574        Some(value) => {
575            alt.b = value.as_u64().map(|v| v as usize).ok_or_else(|| {
576                GrammarError(format!("Grammar: {label}.b must be a non-negative integer"))
577            })?;
578        }
579    }
580    alt.p = string_field(map, "p", label)?;
581    alt.r = string_field(map, "r", label)?;
582    if let Some(reference) = alt.p.as_deref().filter(|value| value.starts_with('@')) {
583        if let Some(route) = refs.match_pushes.get(reference) {
584            alt.p_match = Some(route.clone());
585        } else {
586            alt.p_fn = Some(refs.pushes.get(reference).cloned().ok_or_else(|| {
587                GrammarError(format!(
588                    "Grammar: unknown push function reference: {reference}"
589                ))
590            })?);
591        }
592        alt.p = None;
593    }
594    if let Some(reference) = alt.r.as_deref().filter(|value| value.starts_with('@')) {
595        if let Some(route) = refs.match_replaces.get(reference) {
596            alt.r_match = Some(route.clone());
597        } else {
598            alt.r_fn = Some(refs.replaces.get(reference).cloned().ok_or_else(|| {
599                GrammarError(format!(
600                    "Grammar: unknown replace function reference: {reference}"
601                ))
602            })?);
603        }
604        alt.r = None;
605    }
606    alt.a = match map.get("a") {
607        None | Some(JsonValue::Null) | Some(JsonValue::Bool(false)) => Vec::new(),
608        Some(JsonValue::String(action)) => vec![action.clone()],
609        Some(JsonValue::Array(actions)) => actions
610            .iter()
611            .map(|action| {
612                action.as_str().map(str::to_owned).ok_or_else(|| {
613                    GrammarError(format!("Grammar: {label}.a entries must be strings"))
614                })
615            })
616            .collect::<Result<_, _>>()?,
617        Some(_) => {
618            return Err(GrammarError(format!(
619                "Grammar: {label}.a must be a string or array"
620            )))
621        }
622    };
623    match map.get("c") {
624        Some(JsonValue::String(reference)) => {
625            if matches!(
626                reference.as_str(),
627                "@probePhase0$" | "@probePhase1$" | "@probePhase2$"
628            ) {
629                alt.c_ref = Some(reference.clone());
630            } else if let Some(condition) = refs.conditions.get(reference) {
631                alt.c_fn = Some(condition.clone());
632            } else if let Some(condition) = refs.lexer_match_conditions.get(reference) {
633                alt.c_lex_match = Some(condition.clone());
634            } else if let Some(condition) = refs.match_conditions.get(reference) {
635                alt.c_match = Some(condition.clone());
636            } else if let Some(condition) = refs.lexer_conditions.get(reference) {
637                alt.c_lex = Some(condition.clone());
638            } else {
639                return Err(GrammarError(format!(
640                    "Grammar: unknown condition function reference: {reference}"
641                )));
642            }
643        }
644        value => alt.c = parse_conditions(value, label)?,
645    }
646    if let Some(reference) = optional_ref_field(map, "h", label)? {
647        if let Some(modifier) = refs.match_modifiers.get(&reference) {
648            alt.h_match = Some(modifier.clone());
649        } else {
650            alt.h = Some(refs.modifiers.get(&reference).cloned().ok_or_else(|| {
651                GrammarError(format!(
652                    "Grammar: unknown modifier function reference: {reference}"
653                ))
654            })?);
655        }
656    }
657    if let Some(reference) = optional_ref_field(map, "e", label)? {
658        if let Some(error) = refs.match_errors.get(&reference) {
659            alt.e_match = Some(error.clone());
660        } else {
661            alt.e = Some(refs.errors.get(&reference).cloned().ok_or_else(|| {
662                GrammarError(format!(
663                    "Grammar: unknown error function reference: {reference}"
664                ))
665            })?);
666        }
667    }
668    alt.n = number_map(map.get("n"), label)?;
669    alt.u = value_map(map.get("u"), label)?;
670    alt.k = value_map(map.get("k"), label)?;
671    for action in &alt.a {
672        if matches!(
673            action.as_str(),
674            "@node$"
675                | "@capture$"
676                | "@fold$"
677                | "@object$"
678                | "@array$"
679                | "@key$"
680                | "@setval$"
681                | "@push$"
682                | "@value$"
683        ) {
684            let key = action.trim_start_matches('@');
685            if let Some(config) = alt.k.remove(key) {
686                validate_builtin_config(action, &config, label)?;
687                alt.action_configs.insert(action.clone(), config);
688            }
689        }
690    }
691    alt.g = match map.get("g") {
692        None => String::new(),
693        Some(JsonValue::String(tags)) => tags.clone(),
694        Some(JsonValue::Array(tags)) => tags
695            .iter()
696            .filter_map(JsonValue::as_str)
697            .collect::<Vec<_>>()
698            .join(","),
699        Some(_) => {
700            return Err(GrammarError(format!(
701                "Grammar: {label}.g must be a string or array"
702            )))
703        }
704    };
705    validate_group_tags(&alt.g, label)?;
706    Ok(alt)
707}
708
709fn optional_ref_field(
710    map: &Map<String, JsonValue>,
711    key: &str,
712    label: &str,
713) -> Result<Option<String>, GrammarError> {
714    match map.get(key) {
715        None | Some(JsonValue::Null) => Ok(None),
716        Some(JsonValue::String(reference)) if reference.starts_with('@') => {
717            Ok(Some(reference.clone()))
718        }
719        Some(_) => Err(GrammarError(format!(
720            "Grammar: {label}.{key} must be a function reference"
721        ))),
722    }
723}
724
725fn validate_action_references(tabnas: &Tabnas, spec: &RuleSpec) -> Result<(), GrammarError> {
726    for (state, alts) in [("open", &spec.open), ("close", &spec.close)] {
727        for (index, alt) in alts.iter().enumerate() {
728            for action in &alt.a {
729                if !is_builtin_action(action)
730                    && !tabnas.actions.contains_key(action)
731                    && !tabnas.context_actions.contains_key(action)
732                    && !tabnas.matched_actions.contains_key(action)
733                {
734                    return Err(GrammarError(format!(
735                        "Grammar: {}.{state} alt[{index}]: unknown action function reference: {action}",
736                        spec.name
737                    )));
738                }
739            }
740        }
741    }
742    for (phase, actions) in [
743        ("bo", &spec.bo),
744        ("ao", &spec.ao),
745        ("bc", &spec.bc),
746        ("ac", &spec.ac),
747    ] {
748        for action in actions {
749            if !is_builtin_action(action)
750                && !tabnas.actions.contains_key(action)
751                && !tabnas.context_actions.contains_key(action)
752                && !tabnas.state_actions.contains_key(action)
753            {
754                return Err(GrammarError(format!(
755                    "Grammar: {}.{phase}: unknown state action function reference: {action}",
756                    spec.name
757                )));
758            }
759        }
760    }
761    Ok(())
762}
763
764fn wire_state_actions(tabnas: &Tabnas, spec: &mut RuleSpec) {
765    let has_action = |name: &str| {
766        tabnas.context_actions.contains_key(name) || tabnas.state_actions.contains_key(name)
767    };
768    let rule_name = spec.name.clone();
769    for (phase, actions) in [
770        ("bo", &mut spec.bo),
771        ("ao", &mut spec.ao),
772        ("bc", &mut spec.bc),
773        ("ac", &mut spec.ac),
774    ] {
775        let base = format!("@{rule_name}-{phase}");
776        let replacement = format!("{base}/replace");
777        if has_action(&replacement) {
778            actions.clear();
779            actions.push(replacement);
780            continue;
781        }
782
783        let prepend = format!("{base}/prepend");
784        if has_action(&prepend) && !actions.contains(&prepend) {
785            actions.insert(0, prepend);
786        }
787
788        let explicit_append = format!("{base}/append");
789        let append = has_action(&explicit_append)
790            .then_some(explicit_append)
791            .or_else(|| has_action(&base).then_some(base));
792        if let Some(append) = append.filter(|append| !actions.contains(append)) {
793            actions.push(append);
794        }
795    }
796}
797
798fn validate_builtin_config(action: &str, config: &Value, label: &str) -> Result<(), GrammarError> {
799    let Value::Object(config) = config else {
800        return Err(GrammarError(format!(
801            "Grammar: {label}.k.{} must be an object",
802            action.trim_start_matches('@')
803        )));
804    };
805    let fields: &[(&str, &str)] = match action {
806        "@node$" => &[
807            ("init", "boolean"),
808            ("rule", "string"),
809            ("kind", "string"),
810            ("nterms", "non-negative integer"),
811        ],
812        "@capture$" => &[("rule", "string"), ("kind", "string")],
813        "@fold$" => &[("cN", "non-negative integer")],
814        "@object$" | "@array$" => &[("implicit", "boolean")],
815        "@key$" => &[("slot", "string"), ("from", "integer"), ("lit", "string")],
816        "@setval$" => &[("slot", "string"), ("src", "boolean")],
817        // `chain` is accepted and ignored here. It tells the GO engine
818        // not to re-publish a grown list back along a replacement chain,
819        // which is O(elements^2) there because a Go slice is a value.
820        // This port hands out one `Arc`ed container, so there is nothing
821        // to re-publish and nothing to skip -- but the key has to be
822        // known, or a grammar that declares it is refused at load and
823        // the three ports stop sharing one grammar.
824        "@push$" => &[("src", "boolean"), ("chain", "boolean")],
825        "@value$" => &[("from", "integer")],
826        _ => return Ok(()),
827    };
828    for key in config.keys() {
829        if !fields.iter().any(|(field, _)| key == field) {
830            return Err(GrammarError(format!(
831                "Grammar: {label}.k.{} has unknown field {key}",
832                action.trim_start_matches('@')
833            )));
834        }
835    }
836    for (field, expected) in fields {
837        let Some(value) = config.get(*field) else {
838            continue;
839        };
840        let valid = match *expected {
841            "boolean" => matches!(value, Value::Bool(_)),
842            "string" => matches!(value, Value::String(_)),
843            "integer" => {
844                matches!(value, Value::Number(number) if number.is_finite() && number.fract() == 0.0)
845            }
846            "non-negative integer" => {
847                matches!(value, Value::Number(number) if number.is_finite() && *number >= 0.0 && number.fract() == 0.0)
848            }
849            _ => false,
850        };
851        if !valid {
852            return Err(GrammarError(format!(
853                "Grammar: {label}.k.{}.{} must be a {expected}",
854                action.trim_start_matches('@'),
855                field
856            )));
857        }
858    }
859    Ok(())
860}
861
862fn parse_conditions(
863    value: Option<&JsonValue>,
864    label: &str,
865) -> Result<Vec<Condition>, GrammarError> {
866    let Some(value) = value else {
867        return Ok(Vec::new());
868    };
869    if value.is_null() {
870        return Ok(Vec::new());
871    }
872    let conditions = object(value, &format!("{label}.c"))?;
873    let roots = [
874        "n", "u", "k", "d", "i", "name", "state", "node", "need", "oN", "cN", "o", "c", "o0", "o1",
875        "c0", "c1", "parent", "child", "prev", "next", "spec",
876    ];
877    let mut output = Vec::new();
878    for (path, definition) in conditions {
879        if definition.is_null() {
880            continue;
881        }
882        let parts: Vec<String> = path.split('.').map(str::to_owned).collect();
883        if !roots.contains(&parts[0].as_str()) {
884            return Err(GrammarError(format!(
885                "{label}: unknown condition path: \"{path}\""
886            )));
887        }
888        if let Some(operators) = definition.as_object() {
889            for (operator, value) in operators {
890                let op = match operator.as_str() {
891                    "$eq" => CompareOp::Eq,
892                    "$ne" => CompareOp::Ne,
893                    "$lt" => CompareOp::Lt,
894                    "$lte" => CompareOp::Lte,
895                    "$gt" => CompareOp::Gt,
896                    "$gte" => CompareOp::Gte,
897                    "$exist" => CompareOp::Exist,
898                    _ => {
899                        return Err(GrammarError(format!(
900                            "{label}: unknown condition operator: {operator}"
901                        )))
902                    }
903                };
904                output.push(Condition {
905                    path: parts.clone(),
906                    op,
907                    value: Value::from_json(value),
908                });
909            }
910        } else {
911            output.push(Condition {
912                path: parts,
913                op: CompareOp::Eq,
914                value: Value::from_json(definition),
915            });
916        }
917    }
918    Ok(output)
919}
920
921fn validate_group_tags(tags: &str, label: &str) -> Result<(), GrammarError> {
922    let valid = regex::Regex::new("^[a-z][a-z0-9-]+$").expect("static group regex");
923    for tag in tags.split(',').map(str::trim).filter(|tag| !tag.is_empty()) {
924        if !valid.is_match(tag) {
925            return Err(GrammarError(format!(
926                "{label}: invalid group tag: \"{tag}\""
927            )));
928        }
929    }
930    Ok(())
931}
932
933fn string_field(
934    map: &Map<String, JsonValue>,
935    key: &str,
936    label: &str,
937) -> Result<Option<String>, GrammarError> {
938    match map.get(key) {
939        None | Some(JsonValue::Null) | Some(JsonValue::Bool(false)) => Ok(None),
940        Some(JsonValue::String(value)) => Ok(Some(value.clone())),
941        Some(_) => Err(GrammarError(format!(
942            "Grammar: {label}.{key} must be a string"
943        ))),
944    }
945}
946
947fn number_map(
948    value: Option<&JsonValue>,
949    label: &str,
950) -> Result<HashMap<String, i32>, GrammarError> {
951    let Some(value) = value else {
952        return Ok(HashMap::new());
953    };
954    object(value, &format!("{label}.n"))?
955        .iter()
956        .map(|(key, value)| {
957            value
958                .as_i64()
959                .and_then(|v| i32::try_from(v).ok())
960                .map(|v| (key.clone(), v))
961                .ok_or_else(|| GrammarError(format!("Grammar: {label}.n.{key} must be an integer")))
962        })
963        .collect()
964}
965
966fn value_map(
967    value: Option<&JsonValue>,
968    label: &str,
969) -> Result<HashMap<String, Value>, GrammarError> {
970    let Some(value) = value else {
971        return Ok(HashMap::new());
972    };
973    Ok(object(value, label)?
974        .iter()
975        .map(|(key, value)| (key.clone(), Value::from_json(value)))
976        .collect())
977}
978
979fn apply_lex_check(
980    map: &Map<String, JsonValue>,
981    label: &str,
982    target: &mut Option<LexCheck>,
983    refs: &AltRefs,
984) -> Result<(), GrammarError> {
985    let Some(value) = map.get("check") else {
986        return Ok(());
987    };
988    if value.is_null() || value == &JsonValue::Bool(false) {
989        *target = None;
990        return Ok(());
991    }
992    let reference = value.as_str().ok_or_else(|| {
993        GrammarError(format!(
994            "Grammar: {label}.check must be a function reference or null"
995        ))
996    })?;
997    *target = Some(refs.lex_checks.get(reference).cloned().ok_or_else(|| {
998        GrammarError(format!(
999            "Grammar: unknown lexer check function reference: {reference}"
1000        ))
1001    })?);
1002    Ok(())
1003}
1004
1005fn resolve_value_producer(
1006    value: Option<&JsonValue>,
1007    refs: &AltRefs,
1008) -> (Option<Value>, Option<ValueTransform>) {
1009    match value.filter(|value| !value.is_null()) {
1010        Some(JsonValue::String(reference)) if refs.value_transforms.contains_key(reference) => {
1011            (None, refs.value_transforms.get(reference).cloned())
1012        }
1013        Some(JsonValue::String(reference)) if reference.starts_with("@@") => {
1014            (Some(Value::String(reference[1..].to_string())), None)
1015        }
1016        Some(value) => (Some(Value::from_json(value)), None),
1017        None => (None, None),
1018    }
1019}
1020
1021/// Whether an options path is one the door reads as CODE: the slots a
1022/// function reference may resolve into. Everything else holds data.
1023fn is_code_slot(path: &str) -> bool {
1024    const EXACT: &[&str] = &[
1025        "options.parser.start",
1026        "options.map.merge",
1027        "options.number.exclude",
1028        "options.parse.budget.onCheck",
1029        "options.errmsg.suffix",
1030    ];
1031    if EXACT.contains(&path) {
1032        return true;
1033    }
1034    path.ends_with(".check")
1035        || path.starts_with("options.match.token.")
1036        || path.starts_with("options.match.value.")
1037        || path.starts_with("options.lex.match.")
1038        || path.starts_with("options.parse.prepare.")
1039        || path.starts_with("options.config.modify.")
1040        || path.starts_with("options.text.modify")
1041        || (path.starts_with("options.value.def.") && path.ends_with(".val"))
1042        || (path.starts_with("options.comment.def.") && path.ends_with(".suffix"))
1043}
1044
1045/// A `@name` string that names a function reference, in a slot that
1046/// holds data, is a load fault in every runtime (#143). As in the
1047/// TypeScript door, a string is a reference only when it RESOLVES: a
1048/// builtin's name or a registered one. An unresolvable `@x` stays the
1049/// literal it always was (a fixed token bound to `@`, say), `@@x` is the
1050/// spelled escape for the literal `@x`, and `@/…/` and `@~/…/` are
1051/// serialized regexes, which their own readers decode. The escape is
1052/// applied here, so every data reader sees the literal.
1053fn typed_data_refs(
1054    value: &JsonValue,
1055    path: &str,
1056    refs: &AltRefs,
1057) -> Result<JsonValue, GrammarError> {
1058    match value {
1059        JsonValue::String(text) => {
1060            if is_code_slot(path) {
1061                return Ok(value.clone());
1062            }
1063            if let Some(literal) = text.strip_prefix("@@") {
1064                return Ok(JsonValue::String(format!("@{literal}")));
1065            }
1066            let is_regex = text.starts_with("@/") || text.starts_with("@~/");
1067            if !is_regex && (is_builtin_action(text) || refs.knows(text)) {
1068                return Err(GrammarError(format!(
1069                    "Grammar: {path}: a function reference is not allowed in a data slot \
1070                     (got {text:?}; spell a literal at sign as @@)"
1071                )));
1072            }
1073            Ok(value.clone())
1074        }
1075        JsonValue::Object(entries) => {
1076            let mut out = Map::with_capacity(entries.len());
1077            for (key, entry) in entries {
1078                out.insert(
1079                    key.clone(),
1080                    typed_data_refs(entry, &format!("{path}.{key}"), refs)?,
1081                );
1082            }
1083            Ok(JsonValue::Object(out))
1084        }
1085        JsonValue::Array(items) => Ok(JsonValue::Array(
1086            items
1087                .iter()
1088                .enumerate()
1089                .map(|(index, item)| typed_data_refs(item, &format!("{path}[{index}]"), refs))
1090                .collect::<Result<Vec<_>, _>>()?,
1091        )),
1092        _ => Ok(value.clone()),
1093    }
1094}
1095
1096/// The option maps whose keys a CALLER chooses, dotted and relative to
1097/// `options`. The list mirrors `DYNAMIC_MAPS` in `ts/src/utility.ts`,
1098/// which is where the canonical runtime decides the same question;
1099/// `token_set` is this port's snake alias for the same map.
1100const CALLER_KEYED_MAPS: [&str; 15] = [
1101    "fixed.token",
1102    "match.token",
1103    "match.value",
1104    "tokenSet",
1105    "token_set",
1106    "comment.def",
1107    "value.def",
1108    "string.escape",
1109    "string.replace",
1110    "error",
1111    "hint",
1112    "parse.prepare",
1113    "config.modify",
1114    "lex.match",
1115    "plugin",
1116];
1117
1118/// Refuse the three names the canonical deep merge will not carry, in
1119/// every map whose keys a caller chooses.
1120///
1121/// Rust has no prototype chain and could hold these names happily, but
1122/// the canonical runtime cannot: its merge skips `__proto__`,
1123/// `constructor` and `prototype` on purpose, because merging one of them
1124/// reaches the prototype chain and that is prototype pollution. The CODE
1125/// is the contract across runtimes, so a grammar naming an entry
1126/// `constructor` must not load here and fault there -- and that has to
1127/// hold for EVERY such map rather than for `tokenSet` alone, which is
1128/// where both other runtimes refuse it.
1129///
1130/// One pass over the raw options, before anything is applied, so a
1131/// refused document changes nothing: adding a map to the loader cannot
1132/// silently opt out of the rule, since the list above is the only place
1133/// it is declared.
1134fn reject_reserved_names(map: &Map<String, JsonValue>) -> Result<(), GrammarError> {
1135    for path in CALLER_KEYED_MAPS {
1136        let mut node = map;
1137        let mut segments = path.split('.').peekable();
1138        let entries = loop {
1139            let Some(segment) = segments.next() else {
1140                break None;
1141            };
1142            let Some(child) = node.get(segment).and_then(JsonValue::as_object) else {
1143                break None;
1144            };
1145            if segments.peek().is_none() {
1146                break Some(child);
1147            }
1148            node = child;
1149        };
1150        let Some(entries) = entries else { continue };
1151        for name in entries.keys() {
1152            if RESERVED_MAP_KEYS.contains(&name.as_str()) {
1153                let kind = path.rsplit('.').next().unwrap_or(path);
1154                return Err(GrammarError(format!(
1155                    "Grammar: options.{path}.{name}: `{name}` is a reserved name \
1156                     and cannot be a {kind} entry (it would reach the prototype chain)"
1157                )));
1158            }
1159        }
1160    }
1161    Ok(())
1162}
1163
1164fn apply_options(
1165    options: &mut crate::Options,
1166    map: &Map<String, JsonValue>,
1167    refs: &AltRefs,
1168) -> Result<(), GrammarError> {
1169    let typed = typed_data_refs(&JsonValue::Object(map.clone()), "options", refs)?;
1170    let JsonValue::Object(typed) = typed else {
1171        unreachable!("an object maps to an object");
1172    };
1173    let map = &typed;
1174    reject_reserved_names(map)?;
1175    if let Some(tag) = map.get("tag").and_then(JsonValue::as_str) {
1176        options.tag = tag.into();
1177    }
1178    if let Some(plugin) = map.get("plugin") {
1179        for (name, value) in object(plugin, "options.plugin")? {
1180            if value.is_null() || value == &JsonValue::Bool(false) {
1181                options.plugin.shift_remove(&name.to_lowercase());
1182                continue;
1183            }
1184            let name = name.to_lowercase();
1185            let current = options
1186                .plugin
1187                .shift_remove(&name)
1188                .unwrap_or_else(|| Value::object(IndexMap::new()));
1189            options.plugin.insert(
1190                name,
1191                crate::merge_plugin_values(current, Value::from_json(value)),
1192            );
1193        }
1194    }
1195    for (field, target) in [("error", &mut options.error), ("hint", &mut options.hint)] {
1196        if let Some(overrides) = map.get(field) {
1197            for (code, template) in object(overrides, &format!("options.{field}"))? {
1198                if template.is_null() || template == &JsonValue::Bool(false) {
1199                    target.remove(code);
1200                } else {
1201                    target.insert(
1202                        code.clone(),
1203                        template
1204                            .as_str()
1205                            .ok_or_else(|| {
1206                                GrammarError(format!(
1207                                    "Grammar: options.{field}.{code} must be a string or null"
1208                                ))
1209                            })?
1210                            .into(),
1211                    );
1212                }
1213            }
1214        }
1215    }
1216    if let Some(errmsg) = map.get("errmsg") {
1217        let errmsg = object(errmsg, "options.errmsg")?;
1218        if let Some(name) = errmsg.get("name") {
1219            options.errmsg.name = name
1220                .as_str()
1221                .ok_or_else(|| {
1222                    GrammarError("Grammar: options.errmsg.name must be a string".into())
1223                })?
1224                .into();
1225        }
1226        if let Some(suffix) = errmsg.get("suffix") {
1227            options.errmsg.suffix = match suffix {
1228                JsonValue::Null | JsonValue::Bool(false) => crate::ErrorSuffix::Disabled,
1229                JsonValue::Bool(true) => crate::ErrorSuffix::Standard,
1230                JsonValue::String(reference) => {
1231                    refs.error_suffixes.get(reference).cloned().map_or_else(
1232                        || crate::ErrorSuffix::Text(reference.clone()),
1233                        crate::ErrorSuffix::Callback,
1234                    )
1235                }
1236                _ => {
1237                    return Err(GrammarError(
1238                        "Grammar: options.errmsg.suffix must be a boolean, string, or null".into(),
1239                    ))
1240                }
1241            };
1242        }
1243        if let Some(link) = errmsg.get("link") {
1244            options.errmsg.link = if link.is_null() {
1245                String::new()
1246            } else {
1247                link.as_str()
1248                    .ok_or_else(|| {
1249                        GrammarError("Grammar: options.errmsg.link must be a string or null".into())
1250                    })?
1251                    .into()
1252            };
1253        }
1254    }
1255    if let Some(color) = map.get("color") {
1256        let color = object(color, "options.color")?;
1257        set_bool(color, "active", &mut options.color.active);
1258        for (field, target) in [
1259            ("reset", &mut options.color.reset),
1260            ("hi", &mut options.color.hi),
1261            ("lo", &mut options.color.lo),
1262            ("line", &mut options.color.line),
1263        ] {
1264            if let Some(value) = color.get(field) {
1265                *target = value
1266                    .as_str()
1267                    .ok_or_else(|| {
1268                        GrammarError(format!("Grammar: options.color.{field} must be a string"))
1269                    })?
1270                    .into();
1271            }
1272        }
1273    }
1274    if let Some(text) = map.get("text") {
1275        let text = object(text, "options.text")?;
1276        set_bool(text, "lex", &mut options.text.lex);
1277        apply_lex_check(text, "options.text", &mut options.text.check, refs)?;
1278        if let Some(modify) = text.get("modify") {
1279            let references =
1280                match modify {
1281                    JsonValue::Null | JsonValue::Bool(false) => Vec::new(),
1282                    JsonValue::String(reference) => vec![reference.as_str()],
1283                    JsonValue::Array(references) => references
1284                        .iter()
1285                        .map(|reference| {
1286                            reference.as_str().ok_or_else(|| {
1287                                GrammarError(
1288                                "Grammar: options.text.modify entries must be function references"
1289                                    .into(),
1290                            )
1291                            })
1292                        })
1293                        .collect::<Result<Vec<_>, _>>()?,
1294                    _ => return Err(GrammarError(
1295                        "Grammar: options.text.modify must be a function reference, array, or null"
1296                            .into(),
1297                    )),
1298                };
1299            options.text.modify = references
1300                .into_iter()
1301                .map(|reference| {
1302                    refs.text_modifiers.get(reference).cloned().ok_or_else(|| {
1303                        GrammarError(format!(
1304                            "Grammar: unknown text modifier function reference: {reference}"
1305                        ))
1306                    })
1307                })
1308                .collect::<Result<_, _>>()?;
1309        }
1310    }
1311    if let Some(space) = map.get("space") {
1312        let space = object(space, "options.space")?;
1313        set_bool(space, "lex", &mut options.space.lex);
1314        apply_lex_check(space, "options.space", &mut options.space.check, refs)?;
1315        if let Some(chars) = space.get("chars") {
1316            options.space.chars = chars
1317                .as_str()
1318                .ok_or_else(|| {
1319                    GrammarError("Grammar: options.space.chars must be a string".into())
1320                })?
1321                .into();
1322        }
1323    }
1324    if let Some(number) = map.get("number") {
1325        let number = object(number, "options.number")?;
1326        set_bool(number, "lex", &mut options.number.lex);
1327        set_bool(number, "hex", &mut options.number.hex);
1328        set_bool(number, "oct", &mut options.number.oct);
1329        set_bool(number, "bin", &mut options.number.bin);
1330        apply_lex_check(number, "options.number", &mut options.number.check, refs)?;
1331        if let Some(separator) = number.get("sep") {
1332            options.number.sep = separator.as_str().map(str::to_owned);
1333        }
1334        if let Some(exclude) = number.get("exclude") {
1335            options.number.exclude = exclude.as_str().map(str::to_owned);
1336        }
1337    }
1338    if let Some(string) = map.get("string") {
1339        let string = object(string, "options.string")?;
1340        set_bool(string, "lex", &mut options.string.lex);
1341        apply_lex_check(string, "options.string", &mut options.string.check, refs)?;
1342        if let Some(chars) = string.get("chars").and_then(JsonValue::as_str) {
1343            options.string.chars = chars.into();
1344        }
1345        if let Some(chars) = string
1346            .get("multiChars")
1347            .or_else(|| string.get("multi_chars"))
1348            .and_then(JsonValue::as_str)
1349        {
1350            options.string.multi_chars = chars.into();
1351        }
1352        if let Some(escape_char) = string.get("escapeChar") {
1353            let escape_char = escape_char.as_str().ok_or_else(|| {
1354                GrammarError("Grammar: options.string.escapeChar must be a string".into())
1355            })?;
1356            let mut chars = escape_char.chars();
1357            options.string.escape_char = chars.next().ok_or_else(|| {
1358                GrammarError("Grammar: options.string.escapeChar must not be empty".into())
1359            })?;
1360            if chars.next().is_some() {
1361                return Err(GrammarError(
1362                    "Grammar: options.string.escapeChar must contain one character".into(),
1363                ));
1364            }
1365        }
1366        for (field, target) in [
1367            ("escape", &mut options.string.escape),
1368            ("replace", &mut options.string.replace),
1369        ] {
1370            if let Some(entries) = string.get(field) {
1371                for (key, value) in object(entries, &format!("options.string.{field}"))? {
1372                    let mut chars = key.chars();
1373                    let character = chars.next().ok_or_else(|| {
1374                        GrammarError(format!(
1375                            "Grammar: options.string.{field} keys must not be empty"
1376                        ))
1377                    })?;
1378                    if chars.next().is_some() {
1379                        return Err(GrammarError(format!(
1380                            "Grammar: options.string.{field} keys must contain one character"
1381                        )));
1382                    }
1383                    if value.is_null() {
1384                        target.remove(&character);
1385                    } else {
1386                        target.insert(
1387                            character,
1388                            value
1389                                .as_str()
1390                                .ok_or_else(|| {
1391                                    GrammarError(format!(
1392                                        "Grammar: options.string.{field}.{key} must be a string or null"
1393                                    ))
1394                                })?
1395                                .into(),
1396                        );
1397                    }
1398                }
1399            }
1400        }
1401        if let Some(value) = string
1402            .get("allowUnknown")
1403            .or_else(|| string.get("allow_unknown"))
1404            .and_then(JsonValue::as_bool)
1405        {
1406            options.string.allow_unknown = value;
1407        }
1408        if let Some(value) = string
1409            .get("escapeStrict")
1410            .or_else(|| string.get("escape_strict"))
1411            .and_then(JsonValue::as_bool)
1412        {
1413            options.string.escape_strict = value;
1414        }
1415        if let Some(value) = string
1416            .get("allowControl")
1417            .or_else(|| string.get("allow_control"))
1418            .and_then(JsonValue::as_bool)
1419        {
1420            options.string.allow_control = value;
1421        }
1422        if let Some(value) = string.get("abandon").and_then(JsonValue::as_bool) {
1423            options.string.abandon = value;
1424        }
1425    }
1426    if let Some(line) = map.get("line") {
1427        let line = object(line, "options.line")?;
1428        set_bool(line, "lex", &mut options.line.lex);
1429        set_bool(line, "single", &mut options.line.single);
1430        apply_lex_check(line, "options.line", &mut options.line.check, refs)?;
1431        if let Some(chars) = line.get("chars") {
1432            options.line.chars = chars
1433                .as_str()
1434                .ok_or_else(|| GrammarError("Grammar: options.line.chars must be a string".into()))?
1435                .into();
1436        }
1437        if let Some(chars) = line.get("rowChars") {
1438            options.line.row_chars = chars
1439                .as_str()
1440                .ok_or_else(|| {
1441                    GrammarError("Grammar: options.line.rowChars must be a string".into())
1442                })?
1443                .into();
1444        }
1445        if let Some(chars) = line.get("fixed").and_then(JsonValue::as_str) {
1446            options.line.fixed = chars.chars().collect();
1447        }
1448    }
1449    if let Some(comment) = map.get("comment") {
1450        let comment = object(comment, "options.comment")?;
1451        set_bool(comment, "lex", &mut options.comment.lex);
1452        apply_lex_check(comment, "options.comment", &mut options.comment.check, refs)?;
1453        if let Some(definitions) = comment.get("def") {
1454            for (name, value) in object(definitions, "options.comment.def")? {
1455                if value.is_null() || value == &JsonValue::Bool(false) {
1456                    options.comment.definitions.shift_remove(name);
1457                    continue;
1458                }
1459                let value = object(value, &format!("options.comment.def.{name}"))?;
1460                let definition =
1461                    options
1462                        .comment
1463                        .definitions
1464                        .entry(name.clone())
1465                        .or_insert(crate::CommentDef {
1466                            line: false,
1467                            start: String::new(),
1468                            end: String::new(),
1469                            lex: false,
1470                            suffixes: Vec::new(),
1471                            suffix_matcher: None,
1472                            eat_line: false,
1473                        });
1474                set_bool(value, "line", &mut definition.line);
1475                set_bool(value, "lex", &mut definition.lex);
1476                set_bool(value, "eatline", &mut definition.eat_line);
1477                for (field, target) in [
1478                    ("start", &mut definition.start),
1479                    ("end", &mut definition.end),
1480                ] {
1481                    if let Some(text) = value.get(field) {
1482                        *target = text
1483                            .as_str()
1484                            .ok_or_else(|| {
1485                                GrammarError(format!(
1486                                    "Grammar: options.comment.def.{name}.{field} must be a string"
1487                                ))
1488                            })?
1489                            .into();
1490                    }
1491                }
1492                if let Some(suffix) = value.get("suffix") {
1493                    definition.suffix_matcher = None;
1494                    definition.suffixes = match suffix {
1495                        JsonValue::Null => Vec::new(),
1496                        JsonValue::String(reference)
1497                            if refs.comment_suffixes.contains_key(reference) =>
1498                        {
1499                            definition.suffix_matcher =
1500                                refs.comment_suffixes.get(reference).cloned();
1501                            Vec::new()
1502                        }
1503                        JsonValue::String(suffix) => {
1504                            if suffix.is_empty() {
1505                                Vec::new()
1506                            } else {
1507                                vec![suffix.clone()]
1508                            }
1509                        }
1510                        JsonValue::Array(suffixes) => suffixes
1511                            .iter()
1512                            .map(|suffix| {
1513                                suffix.as_str().map(str::to_owned).ok_or_else(|| {
1514                                    GrammarError(format!(
1515                                        "Grammar: options.comment.def.{name}.suffix entries must be strings"
1516                                    ))
1517                                })
1518                            })
1519                            .collect::<Result<Vec<_>, _>>()?,
1520                        _ => {
1521                            return Err(GrammarError(format!(
1522                                "Grammar: options.comment.def.{name}.suffix must be a string, array, or null"
1523                            )))
1524                        }
1525                    };
1526                    definition.suffixes.retain(|suffix| !suffix.is_empty());
1527                    definition
1528                        .suffixes
1529                        .sort_by(|a, b| b.len().cmp(&a.len()).then_with(|| a.cmp(b)));
1530                }
1531            }
1532        }
1533    }
1534    if let Some(value) = map.get("value") {
1535        let value = object(value, "options.value")?;
1536        set_bool(value, "lex", &mut options.value.lex);
1537        if let Some(definitions) = value.get("def") {
1538            for (name, value) in object(definitions, "options.value.def")? {
1539                if value.is_null() || value == &JsonValue::Bool(false) {
1540                    options.value.definitions.shift_remove(name);
1541                    continue;
1542                }
1543                let value = object(value, &format!("options.value.def.{name}"))?;
1544                let (val, transform) = resolve_value_producer(value.get("val"), refs);
1545                let matcher = value
1546                    .get("match")
1547                    .map(|matcher| {
1548                        let source = matcher.as_str().ok_or_else(|| {
1549                            GrammarError(format!(
1550                                "Grammar: options.value.def.{name}.match must be a serialized regex"
1551                            ))
1552                        })?;
1553                        compile_serialized_regex(
1554                            source,
1555                            &format!("options.value.def.{name}.match"),
1556                            true,
1557                        )
1558                        .map(|(regex, _)| regex)
1559                    })
1560                    .transpose()?;
1561                let consume = match value.get("consume") {
1562                    Some(consume) => consume.as_bool().ok_or_else(|| {
1563                        GrammarError(format!(
1564                            "Grammar: options.value.def.{name}.consume must be a boolean"
1565                        ))
1566                    })?,
1567                    None => false,
1568                };
1569                options.value.definitions.insert(
1570                    name.clone(),
1571                    crate::ValueDef {
1572                        val,
1573                        matcher,
1574                        transform,
1575                        consume,
1576                    },
1577                );
1578            }
1579        }
1580    }
1581    if let Some(ender) = map.get("ender") {
1582        options.ender = match ender {
1583            JsonValue::Null => Vec::new(),
1584            // A string ender is its CHARACTERS, one ender each, as
1585            // TypeScript's `opts.ender.split('')` reads it — not one
1586            // multi-character ender. Single-character, which is what a
1587            // grammar passes, is the same either way.
1588            JsonValue::String(ender) => ender.chars().map(String::from).collect(),
1589            JsonValue::Array(enders) => enders
1590                .iter()
1591                .map(|ender| {
1592                    ender.as_str().map(str::to_owned).ok_or_else(|| {
1593                        GrammarError("Grammar: options.ender entries must be strings".into())
1594                    })
1595                })
1596                .collect::<Result<_, _>>()?,
1597            _ => {
1598                return Err(GrammarError(
1599                    "Grammar: options.ender must be a string, array, or null".into(),
1600                ))
1601            }
1602        };
1603        options.ender.retain(|ender| !ender.is_empty());
1604    }
1605    if let Some(map_options) = map.get("map") {
1606        let map_options = object(map_options, "options.map")?;
1607        set_bool(map_options, "extend", &mut options.map.extend);
1608        set_bool(map_options, "child", &mut options.map.child);
1609        set_bool(map_options, "ordered", &mut options.map.ordered);
1610        if let Some(reference) = map_options.get("merge") {
1611            options.map.merge = match reference {
1612                JsonValue::Null | JsonValue::Bool(false) => None,
1613                JsonValue::String(reference) => {
1614                    Some(refs.map_merges.get(reference).cloned().ok_or_else(|| {
1615                        GrammarError(format!(
1616                            "Grammar: unknown map merge function reference: {reference}"
1617                        ))
1618                    })?)
1619                }
1620                _ => {
1621                    return Err(GrammarError(
1622                        "Grammar: options.map.merge must be a function reference or null".into(),
1623                    ))
1624                }
1625            };
1626        }
1627    }
1628    if let Some(list_options) = map.get("list") {
1629        let list_options = object(list_options, "options.list")?;
1630        set_bool(list_options, "property", &mut options.list.property);
1631        set_bool(list_options, "pair", &mut options.list.pair);
1632        set_bool(list_options, "child", &mut options.list.child);
1633    }
1634    if let Some(safe_options) = map.get("safe") {
1635        let safe_options = object(safe_options, "options.safe")?;
1636        set_bool(safe_options, "key", &mut options.safe.key);
1637    }
1638    if let Some(info) = map.get("info") {
1639        let info = object(info, "options.info")?;
1640        set_bool(info, "map", &mut options.info.map);
1641        set_bool(info, "list", &mut options.info.list);
1642        set_bool(info, "text", &mut options.info.text);
1643        if let Some(marker) = info.get("marker") {
1644            options.info.marker = marker
1645                .as_str()
1646                .ok_or_else(|| {
1647                    GrammarError("Grammar: options.info.marker must be a string".into())
1648                })?
1649                .to_string();
1650        }
1651    }
1652    if let Some(lex) = map.get("lex") {
1653        let lex = object(lex, "options.lex")?;
1654        set_bool(lex, "empty", &mut options.lex.empty);
1655        set_bool(lex, "relex", &mut options.lex.relex);
1656        if let Some(value) = lex.get("emptyResult") {
1657            options.lex.empty_result = Value::from_json(value);
1658        }
1659        if let Some(matchers) = lex.get("match") {
1660            for (name, spec) in object(matchers, "options.lex.match")? {
1661                if spec.is_null() || spec == &JsonValue::Bool(false) {
1662                    options.lex.matchers.shift_remove(name);
1663                    continue;
1664                }
1665                let spec = object(spec, &format!("options.lex.match.{name}"))?;
1666                let order = spec
1667                    .get("order")
1668                    .and_then(JsonValue::as_f64)
1669                    .filter(|order| order.is_finite())
1670                    .ok_or_else(|| {
1671                        GrammarError(format!(
1672                            "Grammar: options.lex.match.{name}.order must be a finite number"
1673                        ))
1674                    })?;
1675                if order < 0.0 {
1676                    options.lex.matchers.shift_remove(name);
1677                    continue;
1678                }
1679                let reference = spec
1680                    .get("make")
1681                    .and_then(JsonValue::as_str)
1682                    .ok_or_else(|| {
1683                        GrammarError(format!(
1684                            "Grammar: options.lex.match.{name}.make must be a function reference"
1685                        ))
1686                    })?;
1687                let matcher = refs.lex_matches.get(reference).cloned();
1688                let imperative = refs.imperative_lex_matches.get(reference).cloned();
1689                let factory = refs.lex_match_factories.get(reference).cloned();
1690                if matcher.is_none() && imperative.is_none() && factory.is_none() {
1691                    return Err(GrammarError(format!(
1692                        "Grammar: unknown custom lexer matcher function reference: {reference}"
1693                    )));
1694                }
1695                options.lex.matchers.insert(
1696                    name.clone(),
1697                    crate::LexMatcher {
1698                        name: name.clone(),
1699                        order,
1700                        matcher,
1701                        imperative,
1702                        factory,
1703                    },
1704                );
1705            }
1706            options.lex.matchers.sort_by(|name_a, left, name_b, right| {
1707                left.order
1708                    .total_cmp(&right.order)
1709                    .then_with(|| name_a.cmp(name_b))
1710            });
1711        }
1712    }
1713    if let Some(rewind) = map.get("rewind") {
1714        let rewind = object(rewind, "options.rewind")?;
1715        if let Some(history) = rewind.get("history") {
1716            // The cross-runtime spellings: `null` is the documented
1717            // default, `false` retains everything, a negative cap
1718            // retains nothing, exactly as 0 does.
1719            options.rewind.history = match history {
1720                JsonValue::Null => crate::RewindOptions::default().history,
1721                JsonValue::Bool(false) => None,
1722                JsonValue::Number(number) => match number.as_i64() {
1723                    Some(value) if value < 0 => Some(0),
1724                    Some(value) => Some(usize::try_from(value).map_err(|_| {
1725                        GrammarError(
1726                            "Grammar: options.rewind.history is outside the supported range".into(),
1727                        )
1728                    })?),
1729                    None => {
1730                        return Err(GrammarError(
1731                            "Grammar: options.rewind.history must be an integer, null or false"
1732                                .into(),
1733                        ))
1734                    }
1735                },
1736                _ => {
1737                    return Err(GrammarError(
1738                        "Grammar: options.rewind.history must be an integer, null or false".into(),
1739                    ))
1740                }
1741            };
1742        }
1743    }
1744    if let Some(rule) = map.get("rule") {
1745        let rule = object(rule, "options.rule")?;
1746        if let Some(start) = rule.get("start").and_then(JsonValue::as_str) {
1747            options.rule.start = start.into();
1748        }
1749        if let Some(finish) = rule.get("finish").and_then(JsonValue::as_bool) {
1750            options.rule.finish = finish;
1751        }
1752        if let Some(maxmul) = rule.get("maxmul") {
1753            options.rule.maxmul = match maxmul.as_i64() {
1754                Some(value) if value <= 0 => 3,
1755                Some(value) => usize::try_from(value).map_err(|_| {
1756                    GrammarError(
1757                        "Grammar: options.rule.maxmul is outside the supported range".into(),
1758                    )
1759                })?,
1760                None => {
1761                    return Err(GrammarError(
1762                        "Grammar: options.rule.maxmul must be an integer".into(),
1763                    ))
1764                }
1765            };
1766        }
1767        if let Some(include) = rule.get("include").and_then(JsonValue::as_str) {
1768            options.rule.include = include.into();
1769        }
1770        if let Some(exclude) = rule.get("exclude").and_then(JsonValue::as_str) {
1771            options.rule.exclude = exclude.into();
1772        }
1773        // An integer of at least 1, and at most `MAX_RULE_HISTORY`,
1774        // bounds the chain; `null` or `false` keeps every link, as the
1775        // default does.
1776        if let Some(history) = rule.get("history") {
1777            options.rule.history =
1778                match history {
1779                    JsonValue::Null | JsonValue::Bool(false) => None,
1780                    JsonValue::Number(_) => match history.as_i64() {
1781                        Some(value) if value >= 1 => Some(
1782                            usize::try_from(value)
1783                                .ok()
1784                                .filter(|value| *value <= crate::options::MAX_RULE_HISTORY)
1785                                .ok_or_else(|| {
1786                                    GrammarError(format!(
1787                                        "Grammar: options.rule.history is outside the supported \
1788                                     range (at most {})",
1789                                        crate::options::MAX_RULE_HISTORY
1790                                    ))
1791                                })?,
1792                        ),
1793                        _ => return Err(GrammarError(
1794                            "Grammar: options.rule.history must be an integer of at least 1, null \
1795                             or false"
1796                                .into(),
1797                        )),
1798                    },
1799                    _ => return Err(GrammarError(
1800                        "Grammar: options.rule.history must be an integer of at least 1, null or \
1801                         false"
1802                            .into(),
1803                    )),
1804                };
1805        }
1806    }
1807    if let Some(parse) = map.get("parse") {
1808        let parse = object(parse, "options.parse")?;
1809        if let Some(prepare) = parse.get("prepare") {
1810            let prepare = object(prepare, "options.parse.prepare")?;
1811            for (name, reference) in prepare {
1812                if reference.is_null() || reference == &JsonValue::Bool(false) {
1813                    options.parse.named_prepare.shift_remove(name);
1814                    continue;
1815                }
1816                let reference = reference.as_str().ok_or_else(|| {
1817                    GrammarError(format!(
1818                        "Grammar: options.parse.prepare.{name} must be a function reference or null"
1819                    ))
1820                })?;
1821                let callback = refs.parse_prepares.get(reference).cloned().ok_or_else(|| {
1822                    GrammarError(format!(
1823                        "Grammar: unknown parse prepare function reference: {reference}"
1824                    ))
1825                })?;
1826                options.parse.named_prepare.insert(name.clone(), callback);
1827            }
1828            options.parse.named_prepare.sort_keys();
1829        }
1830        if let Some(budget) = parse.get("budget") {
1831            let budget = object(budget, "options.parse.budget")?;
1832            if let Some(interval) = budget
1833                .get("checkEveryN")
1834                .or_else(|| budget.get("check_every_n"))
1835            {
1836                options.parse.budget.check_every_n = interval
1837                    .as_u64()
1838                    .and_then(|value| usize::try_from(value).ok())
1839                    .ok_or_else(|| {
1840                        GrammarError(
1841                            "Grammar: options.parse.budget.checkEveryN must be a non-negative integer"
1842                                .into(),
1843                        )
1844                })?;
1845            }
1846            if let Some(reference) = budget.get("onCheck").or_else(|| budget.get("on_check")) {
1847                options.parse.budget.on_check = match reference {
1848                    JsonValue::Null | JsonValue::Bool(false) => None,
1849                    JsonValue::String(reference) => Some(
1850                        refs.budget_checks
1851                            .get(reference)
1852                            .cloned()
1853                            .ok_or_else(|| {
1854                                GrammarError(format!(
1855                                    "Grammar: unknown parse budget function reference: {reference}"
1856                                ))
1857                            })?,
1858                    ),
1859                    _ => {
1860                        return Err(GrammarError(
1861                            "Grammar: options.parse.budget.onCheck must be a function reference or null"
1862                                .into(),
1863                        ))
1864                    }
1865                };
1866            }
1867        }
1868        if let Some(recover) = parse.get("recover") {
1869            let recover = object(recover, "options.parse.recover")?;
1870            set_bool(recover, "enabled", &mut options.parse.recover.enabled);
1871            set_bool(
1872                recover,
1873                "popUntilValid",
1874                &mut options.parse.recover.pop_until_valid,
1875            );
1876            if let Some(groups) = recover.get("syncGroups") {
1877                options.parse.recover.sync_groups = match groups {
1878                    JsonValue::Null => vec!["close".into(), "comma".into(), "end".into()],
1879                    JsonValue::Array(groups) => groups
1880                        .iter()
1881                        .map(|group| {
1882                            group.as_str().map(str::to_owned).ok_or_else(|| {
1883                                GrammarError(
1884                                    "Grammar: options.parse.recover.syncGroups entries must be strings"
1885                                        .into(),
1886                                )
1887                            })
1888                        })
1889                        .collect::<Result<_, _>>()?,
1890                    _ => {
1891                        return Err(GrammarError(
1892                            "Grammar: options.parse.recover.syncGroups must be an array or null"
1893                                .into(),
1894                        ))
1895                    }
1896                };
1897            }
1898            if let Some(tokens) = recover.get("syncTokens") {
1899                options.parse.recover.sync_tokens = tokens
1900                    .as_array()
1901                    .ok_or_else(|| {
1902                        GrammarError(
1903                            "Grammar: options.parse.recover.syncTokens must be an array".into(),
1904                        )
1905                    })?
1906                    .iter()
1907                    .map(|token| {
1908                        token.as_str().map(str::to_owned).ok_or_else(|| {
1909                            GrammarError(
1910                                "Grammar: options.parse.recover.syncTokens entries must be strings"
1911                                    .into(),
1912                            )
1913                        })
1914                    })
1915                    .collect::<Result<_, _>>()?;
1916            }
1917            for (name, target) in [
1918                ("maxSkip", &mut options.parse.recover.max_skip),
1919                ("maxRecoveries", &mut options.parse.recover.max_recoveries),
1920                ("suppress", &mut options.parse.recover.suppress),
1921            ] {
1922                if let Some(value) = recover.get(name) {
1923                    *target = value
1924                        .as_u64()
1925                        .and_then(|value| usize::try_from(value).ok())
1926                        .ok_or_else(|| {
1927                            GrammarError(format!(
1928                                "Grammar: options.parse.recover.{name} must be a non-negative integer"
1929                            ))
1930                        })?;
1931                }
1932            }
1933        }
1934    }
1935    if let Some(result) = map.get("result") {
1936        let result = object(result, "options.result")?;
1937        if let Some(fail) = result.get("fail") {
1938            options.result.fail = fail
1939                .as_array()
1940                .ok_or_else(|| {
1941                    GrammarError("Grammar: options.result.fail must be an array".into())
1942                })?
1943                .iter()
1944                .map(Value::from_json)
1945                .collect();
1946        }
1947    }
1948    if let Some(debug) = map.get("debug") {
1949        let debug = object(debug, "options.debug")?;
1950        if let Some(maxlen) = debug.get("maxlen") {
1951            options.debug.maxlen = maxlen
1952                .as_u64()
1953                .and_then(|value| usize::try_from(value).ok())
1954                .ok_or_else(|| {
1955                    GrammarError(
1956                        "Grammar: options.debug.maxlen must be a non-negative integer".into(),
1957                    )
1958                })?;
1959        }
1960        if let Some(print) = debug.get("print") {
1961            let print = object(print, "options.debug.print")?;
1962            set_bool(print, "config", &mut options.debug.print.config);
1963        }
1964    }
1965    if let Some(parser) = map.get("parser") {
1966        let parser = object(parser, "options.parser")?;
1967        if let Some(reference) = parser.get("start") {
1968            match reference {
1969                JsonValue::Null | JsonValue::Bool(false) => {
1970                    options.parser.start = None;
1971                    options.parser.start_with_instance = None;
1972                    options.parser.start_with_context = None;
1973                }
1974                JsonValue::String(reference) => {
1975                    options.parser.start = refs.parser_starts.get(reference).cloned();
1976                    options.parser.start_with_instance =
1977                        refs.parser_starts_with_instance.get(reference).cloned();
1978                    options.parser.start_with_context =
1979                        refs.parser_starts_with_context.get(reference).cloned();
1980                    if options.parser.start.is_none()
1981                        && options.parser.start_with_instance.is_none()
1982                        && options.parser.start_with_context.is_none()
1983                    {
1984                        return Err(GrammarError(format!(
1985                            "Grammar: unknown parser start function reference: {reference}"
1986                        )));
1987                    }
1988                }
1989                _ => {
1990                    return Err(GrammarError(
1991                        "Grammar: options.parser.start must be a function reference or null".into(),
1992                    ))
1993                }
1994            }
1995        }
1996    }
1997    if let Some(fixed_options) = map.get("fixed") {
1998        let fixed_options = object(fixed_options, "options.fixed")?;
1999        set_bool(fixed_options, "lex", &mut options.fixed.lex);
2000        apply_lex_check(
2001            fixed_options,
2002            "options.fixed",
2003            &mut options.fixed.check,
2004            refs,
2005        )?;
2006        if let Some(tokens) = fixed_options.get("token") {
2007            for (name, source) in object(tokens, "options.fixed.token")? {
2008                let name = if name.starts_with('#') {
2009                    name.clone()
2010                } else {
2011                    format!("#{name}")
2012                };
2013                if source.is_null() {
2014                    options.fixed.tokens.shift_remove(&name);
2015                    continue;
2016                }
2017                let source = source.as_str().ok_or_else(|| {
2018                    GrammarError(format!(
2019                        "Grammar: options.fixed.token.{name} must be a string or null"
2020                    ))
2021                })?;
2022                if matches!(
2023                    name.as_str(),
2024                    "#BD"
2025                        | "#ZZ"
2026                        | "#UK"
2027                        | "#AA"
2028                        | "#SP"
2029                        | "#LN"
2030                        | "#CM"
2031                        | "#NR"
2032                        | "#ST"
2033                        | "#TX"
2034                        | "#VL"
2035                ) {
2036                    return Err(GrammarError(format!(
2037                        "Grammar: {name} is produced by a lexer matcher and cannot be bound to a fixed literal"
2038                    )));
2039                }
2040                let tin = options
2041                    .fixed
2042                    .tokens
2043                    .get(&name)
2044                    .map(|token| token.tin)
2045                    .or_else(|| options.token(&name))
2046                    .unwrap_or_else(|| options.next_tin());
2047                options.fixed.tokens.insert(
2048                    name.clone(),
2049                    crate::options::FixedToken {
2050                        name,
2051                        tin,
2052                        source: source.to_string(),
2053                    },
2054                );
2055            }
2056        }
2057    }
2058    if let Some(match_options) = map.get("match") {
2059        let match_options = object(match_options, "options.match")?;
2060        set_bool(match_options, "lex", &mut options.match_lex);
2061        apply_lex_check(
2062            match_options,
2063            "options.match",
2064            &mut options.match_check,
2065            refs,
2066        )?;
2067        if let Some(tokens) = match_options.get("token") {
2068            for (name, source) in object(tokens, "options.match.token")? {
2069                let name = if name.starts_with('#') {
2070                    name.clone()
2071                } else {
2072                    format!("#{name}")
2073                };
2074                if source.is_null() {
2075                    options.match_tokens.shift_remove(&name);
2076                    continue;
2077                }
2078                let source = source.as_str().ok_or_else(|| {
2079                    GrammarError(format!(
2080                        "Grammar: options.match.token.{name} must be a serialized regex"
2081                    ))
2082                })?;
2083                let matcher = if serialized_regex(source).is_some() {
2084                    let (regex, eager) = compile_serialized_regex(
2085                        source,
2086                        &format!("options.match.token.{name}"),
2087                        true,
2088                    )?;
2089                    (crate::MatchTokenMatcher::Regex(regex), eager)
2090                } else if let Some((callback, eager)) = refs.match_tokens.get(source) {
2091                    (crate::MatchTokenMatcher::Callback(callback.clone()), *eager)
2092                } else if source.starts_with('@') {
2093                    return Err(GrammarError(format!(
2094                        "Grammar: unknown token matcher function reference: {source}"
2095                    )));
2096                } else {
2097                    return Err(GrammarError(format!(
2098                        "Grammar: options.match.token.{name} must use @/pattern/flags or a registered function reference"
2099                    )));
2100                };
2101                let tin = options
2102                    .match_tokens
2103                    .get(&name)
2104                    .map(|matcher| matcher.tin)
2105                    .or_else(|| options.token(&name))
2106                    .unwrap_or_else(|| options.next_tin());
2107                options.match_tokens.insert(
2108                    name.clone(),
2109                    crate::options::MatchToken {
2110                        name,
2111                        tin,
2112                        matcher: matcher.0,
2113                        eager: matcher.1,
2114                    },
2115                );
2116            }
2117        }
2118        if let Some(values) = match_options.get("value") {
2119            for (name, value) in object(values, "options.match.value")? {
2120                if value.is_null() || value == &JsonValue::Bool(false) {
2121                    options.match_values.shift_remove(name);
2122                    continue;
2123                }
2124                let value = object(value, &format!("options.match.value.{name}"))?;
2125                let source = value
2126                    .get("match")
2127                    .and_then(JsonValue::as_str)
2128                    .ok_or_else(|| {
2129                        GrammarError(format!(
2130                            "Grammar: options.match.value.{name}.match must be a serialized regex or function reference"
2131                        ))
2132                    })?;
2133                let matcher = if serialized_regex(source).is_some() {
2134                    let (regex, _) = compile_serialized_regex(
2135                        source,
2136                        &format!("options.match.value.{name}.match"),
2137                        true,
2138                    )?;
2139                    crate::MatchTokenMatcher::Regex(regex)
2140                } else if let Some(callback) = refs.match_values.get(source) {
2141                    crate::MatchTokenMatcher::Callback(callback.clone())
2142                } else if source.starts_with('@') {
2143                    return Err(GrammarError(format!(
2144                        "Grammar: unknown value matcher function reference: {source}"
2145                    )));
2146                } else {
2147                    return Err(GrammarError(format!(
2148                        "Grammar: options.match.value.{name}.match must use @/pattern/flags or a registered function reference"
2149                    )));
2150                };
2151                let (val, transform) = resolve_value_producer(value.get("val"), refs);
2152                options.match_values.insert(
2153                    name.clone(),
2154                    crate::MatchValue {
2155                        name: name.clone(),
2156                        matcher,
2157                        val,
2158                        transform,
2159                    },
2160                );
2161            }
2162            options.match_values.sort_keys();
2163        }
2164    }
2165    if let Some(token_sets) = map.get("tokenSet").or_else(|| map.get("token_set")) {
2166        for (name, members) in object(token_sets, "options.tokenSet")? {
2167            // A null whole value falls through to the array check below
2168            // and is a load error, as it is in TypeScript and Go. It
2169            // used to REMOVE the named set here, which made the same
2170            // JSON document mean three different things: a fault in
2171            // TypeScript, a silent no-op in Go, and a deletion here.
2172            // Only a null MEMBER has a meaning, and it clears that
2173            // position -- see the index-wise merge below.
2174            let members = members.as_array().ok_or_else(|| {
2175                GrammarError(format!("Grammar: options.tokenSet.{name} must be an array"))
2176            })?;
2177            // Index-wise onto the set already installed, as TypeScript's
2178            // deep merge treats the array (#151): a `null` removes that
2179            // position, and a shorter array keeps the tail of the set it
2180            // overlays. A wholesale replacement is spelled with explicit
2181            // nulls, which is how the TS grammars spell it.
2182            let key = name.trim_start_matches('#').to_string();
2183            let existing = options.token_set.get(&key).cloned().unwrap_or_default();
2184            let mut tins = Vec::with_capacity(members.len().max(existing.len()));
2185            for member in members {
2186                if member.is_null() {
2187                    continue;
2188                }
2189                let member = member.as_str().ok_or_else(|| {
2190                    GrammarError(format!(
2191                        "Grammar: options.tokenSet.{name} entries must be strings or null"
2192                    ))
2193                })?;
2194                let tin = options
2195                    .token(member)
2196                    .unwrap_or_else(|| options.register_token(member));
2197                tins.push(tin);
2198            }
2199            tins.extend(existing.iter().skip(members.len()).copied());
2200            options.token_set.insert(key, tins);
2201        }
2202    }
2203
2204    if let Some(config) = map.get("config") {
2205        let config = object(config, "options.config")?;
2206        if let Some(modifiers) = config.get("modify") {
2207            for (name, reference) in object(modifiers, "options.config.modify")? {
2208                if reference.is_null() || reference == &JsonValue::Bool(false) {
2209                    options.config_modify.shift_remove(name);
2210                    continue;
2211                }
2212                let reference = reference.as_str().ok_or_else(|| {
2213                    GrammarError(format!(
2214                        "Grammar: options.config.modify.{name} must be a function reference or null"
2215                    ))
2216                })?;
2217                let modifier = refs
2218                    .config_modifiers
2219                    .get(reference)
2220                    .cloned()
2221                    .ok_or_else(|| {
2222                        GrammarError(format!(
2223                            "Grammar: unknown config modifier function reference: {reference}"
2224                        ))
2225                    })?;
2226                options.config_modify.insert(name.clone(), modifier);
2227            }
2228        }
2229    }
2230
2231    Ok(())
2232}
2233
2234fn set_bool(map: &Map<String, JsonValue>, key: &str, target: &mut bool) {
2235    if let Some(value) = map.get(key).and_then(JsonValue::as_bool) {
2236        *target = value;
2237    }
2238}
2239
2240fn serialized_regex(source: &str) -> Option<(&str, &str, bool)> {
2241    let (body, eager) = source
2242        .strip_prefix("@~/")
2243        .map(|body| (body, true))
2244        .or_else(|| source.strip_prefix("@/").map(|body| (body, false)))?;
2245    let slash = body.rfind('/')?;
2246    Some((&body[..slash], &body[slash + 1..], eager))
2247}
2248
2249fn compile_serialized_regex(
2250    source: &str,
2251    label: &str,
2252    reject_empty: bool,
2253) -> Result<(regex::Regex, bool), GrammarError> {
2254    let (pattern, flags, eager) = serialized_regex(source)
2255        .ok_or_else(|| GrammarError(format!("Grammar: {label} must use @/pattern/flags")))?;
2256    if flags.contains('v')
2257        || flags
2258            .chars()
2259            .any(|flag| !matches!(flag, 'i' | 'm' | 's' | 'u' | 'g' | 'y' | 'd'))
2260    {
2261        return Err(GrammarError(format!(
2262            "Grammar: unsupported regex flags: {flags}"
2263        )));
2264    }
2265    let mut builder = RegexBuilder::new(pattern);
2266    builder
2267        .case_insensitive(flags.contains('i'))
2268        .multi_line(flags.contains('m'))
2269        .dot_matches_new_line(flags.contains('s'));
2270    let regex = builder
2271        .build()
2272        .map_err(|error| GrammarError(format!("Grammar: invalid regex for {label}: {error}")))?;
2273    if reject_empty && regex.is_match("") {
2274        return Err(GrammarError(format!(
2275            "Grammar: regex for {label} must not match empty input"
2276        )));
2277    }
2278    Ok((regex, eager))
2279}
2280
2281pub fn validate_grammar(rules: &IndexMap<String, RuleSpec>) -> Vec<String> {
2282    let mut problems = Vec::new();
2283    for (name, spec) in rules {
2284        for (state, alts) in [("open", &spec.open), ("close", &spec.close)] {
2285            for (index, alt) in alts.iter().enumerate() {
2286                for referred in [&alt.p, &alt.r].into_iter().flatten() {
2287                    if !referred.starts_with('@') && !rules.contains_key(referred) {
2288                        problems.push(format!(
2289                            "{name}.{state} alt[{index}]: unknown rule: {referred}"
2290                        ));
2291                    }
2292                }
2293            }
2294        }
2295    }
2296    problems.sort_by(|left, right| left.encode_utf16().cmp(right.encode_utf16()));
2297    problems
2298}