Skip to main content

pictor_runtime/grammar/
json_schema_compiler.rs

1//! JSON Schema → BNF Grammar compiler.
2//!
3//! Compiles a JSON Schema (represented as a `serde_json::Value`) directly
4//! into a [`Grammar`] using [`Grammar::alloc_nt`] and manual rule construction.
5//! No text emission or re-parsing — the grammar is built programmatically,
6//! following the same right-recursive style used in `examples.rs`.
7//!
8//! # Supported JSON Schema v1 features
9//!
10//! - **Primitives:** `string`, `integer`, `number`, `boolean`, `null`
11//! - **Composites:** `object` (with `properties`, `required`,
12//!   `additionalProperties: false`), `array` (with `items`)
13//! - **Constraints:** `enum` (string/integer/boolean/null values)
14//! - **Composition:** `anyOf`, `oneOf` (compiled identically; Earley handles
15//!   ambiguity), `allOf` (merge when all branches are object schemas)
16//! - **References:** `$ref` pointing to `"#/$defs/..."` or `"#/definitions/..."`,
17//!   via two-pass pre-allocation of NT ids.
18//!
19//! # Out of scope (returns `UnsupportedKeyword`)
20//!
21//! `not`, `if`, `then`, `else`, `patternProperties`,
22//! `additionalProperties: <subschema>` (only `false` is allowed),
23//! `pattern`, `format`, `multipleOf`, `minimum`, `maximum`,
24//! `exclusiveMinimum`, `exclusiveMaximum`.
25
26use std::collections::HashMap;
27
28use serde_json::Value;
29
30use super::ast::{Grammar, NonTerminalId, Rule, Symbol};
31
32// ─────────────────────────────────────────────────────────────────────────────
33// Public error type
34// ─────────────────────────────────────────────────────────────────────────────
35
36/// Errors arising from compiling a JSON Schema into a Grammar.
37#[derive(Debug, Clone)]
38pub enum JsonSchemaCompileError {
39    /// The schema JSON is structurally invalid (missing field, wrong type, etc.).
40    InvalidSchema(String),
41    /// A JSON Schema keyword that is not supported by this compiler was used.
42    UnsupportedKeyword(String),
43    /// A `$ref` points to a `$defs`/`definitions` key that does not exist.
44    DanglingRef(String),
45    /// The schema nesting depth exceeded the compiled limit.
46    DepthExceeded {
47        /// Maximum depth allowed.
48        limit: usize,
49    },
50    /// The input string passed to [`compile_json_schema_str`] is not valid JSON.
51    InvalidJson(String),
52}
53
54impl std::fmt::Display for JsonSchemaCompileError {
55    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
56        match self {
57            Self::InvalidSchema(msg) => write!(f, "invalid JSON schema: {msg}"),
58            Self::UnsupportedKeyword(kw) => write!(f, "unsupported JSON schema keyword: {kw}"),
59            Self::DanglingRef(r) => write!(f, "unresolvable $ref: {r}"),
60            Self::DepthExceeded { limit } => {
61                write!(f, "schema nesting depth exceeded limit of {limit}")
62            }
63            Self::InvalidJson(msg) => write!(f, "invalid JSON input: {msg}"),
64        }
65    }
66}
67
68impl std::error::Error for JsonSchemaCompileError {}
69
70// ─────────────────────────────────────────────────────────────────────────────
71// Public API
72// ─────────────────────────────────────────────────────────────────────────────
73
74/// Compile a JSON Schema (as a `serde_json::Value`) into a [`Grammar`].
75///
76/// The returned grammar has not yet been normalised; call
77/// [`Grammar::normalise_terminals`] before passing it to
78/// [`super::constraint::GrammarConstraint`] (the constraint builder does this
79/// automatically).
80///
81/// # Errors
82///
83/// Returns a [`JsonSchemaCompileError`] if the schema is structurally invalid,
84/// uses an unsupported keyword, contains a dangling `$ref`, or exceeds the
85/// maximum recursion depth (32).
86pub fn compile_json_schema(schema: &Value) -> Result<Grammar, JsonSchemaCompileError> {
87    Compiler::new().compile(schema)
88}
89
90/// Compile a JSON Schema from a JSON string.
91///
92/// Parses the string with `serde_json` then delegates to [`compile_json_schema`].
93///
94/// # Errors
95///
96/// Returns [`JsonSchemaCompileError::InvalidJson`] if the input is not valid JSON,
97/// or any other [`JsonSchemaCompileError`] variant if schema compilation fails.
98pub fn compile_json_schema_str(schema_json: &str) -> Result<Grammar, JsonSchemaCompileError> {
99    let value: Value = serde_json::from_str(schema_json)
100        .map_err(|e| JsonSchemaCompileError::InvalidJson(e.to_string()))?;
101    compile_json_schema(&value)
102}
103
104// ─────────────────────────────────────────────────────────────────────────────
105// Internal: compiler state
106// ─────────────────────────────────────────────────────────────────────────────
107
108/// Maximum allowed schema nesting depth.
109const MAX_DEPTH: usize = 32;
110
111/// Internal compiler that accumulates a [`Grammar`] while walking a JSON Schema.
112struct Compiler {
113    grammar: Grammar,
114    /// Maps `$defs` / `definitions` key → pre-allocated NT id (pass 1).
115    defs_nt: HashMap<String, NonTerminalId>,
116    /// Tracks which `$defs` keys have already been compiled in pass 2, to
117    /// handle forward references without infinite loops.
118    defs_compiled: HashMap<String, bool>,
119    /// Shared string NT reused across all `"string"` occurrences (None until
120    /// first use — allocated lazily).
121    string_nt: Option<NonTerminalId>,
122    /// Shared digit NT (0–9) reused for integers and numbers.
123    digit_nt: Option<NonTerminalId>,
124}
125
126impl Compiler {
127    fn new() -> Self {
128        // Start grammar with a placeholder start id=0; the actual start NT
129        // is allocated during compile() and replaces grammar.start.
130        let grammar = Grammar::new(0);
131        Self {
132            grammar,
133            defs_nt: HashMap::new(),
134            defs_compiled: HashMap::new(),
135            string_nt: None,
136            digit_nt: None,
137        }
138    }
139
140    /// Entry point — orchestrates the two-pass compilation.
141    fn compile(mut self, root: &Value) -> Result<Grammar, JsonSchemaCompileError> {
142        // ── Pass 1: pre-allocate NT ids for every $defs / definitions key ───
143        self.pass1_alloc_defs(root);
144
145        // ── Pass 2: compile each definition body ────────────────────────────
146        // We collect the keys first to avoid borrow conflicts.
147        let def_keys: Vec<String> = self.defs_nt.keys().cloned().collect();
148        for key in &def_keys {
149            self.pass2_compile_def(key, root)?;
150        }
151
152        // ── Pass 2: compile the root schema itself ───────────────────────────
153        let start_nt = self.compile_schema(root, 0)?;
154        self.grammar.start = start_nt;
155
156        Ok(self.grammar)
157    }
158
159    // ─────────────────────────────────────────────────────────────────────────
160    // Pass 1 helpers
161    // ─────────────────────────────────────────────────────────────────────────
162
163    /// Scan the top-level `$defs` / `definitions` object and pre-allocate one
164    /// NT id per key.  This makes every `$ref` resolvable even if the ref
165    /// appears before the definition body.
166    fn pass1_alloc_defs(&mut self, root: &Value) {
167        let defs = root
168            .get("$defs")
169            .or_else(|| root.get("definitions"))
170            .and_then(|v| v.as_object());
171
172        if let Some(map) = defs {
173            for key in map.keys() {
174                if !self.defs_nt.contains_key(key) {
175                    let nt = self.grammar.alloc_nt(format!("$def_{key}"));
176                    self.defs_nt.insert(key.clone(), nt);
177                    self.defs_compiled.insert(key.clone(), false);
178                }
179            }
180        }
181    }
182
183    // ─────────────────────────────────────────────────────────────────────────
184    // Pass 2 helpers
185    // ─────────────────────────────────────────────────────────────────────────
186
187    /// Compile the body of definition `key` into rules for the pre-allocated NT.
188    fn pass2_compile_def(&mut self, key: &str, root: &Value) -> Result<(), JsonSchemaCompileError> {
189        // Guard: only compile once.
190        if *self.defs_compiled.get(key).unwrap_or(&false) {
191            return Ok(());
192        }
193        // Mark as compiled early to break potential self-reference cycles.
194        self.defs_compiled.insert(key.to_string(), true);
195
196        let def_nt = *self.defs_nt.get(key).expect("NT pre-allocated in pass 1");
197
198        let body_value = root
199            .get("$defs")
200            .or_else(|| root.get("definitions"))
201            .and_then(|v| v.get(key))
202            .ok_or_else(|| {
203                JsonSchemaCompileError::InvalidSchema(format!("$defs key '{key}' not found"))
204            })?;
205
206        // Compile the definition body — the result NT must equal def_nt.
207        // We use a small trick: compile normally to get an intermediate NT,
208        // then add a bridging rule `def_nt ::= intermediate_nt` if they differ.
209        let compiled_nt = self.compile_schema(body_value, 0)?;
210        if compiled_nt != def_nt {
211            self.grammar
212                .add_rule(Rule::new(def_nt, vec![Symbol::NonTerminal(compiled_nt)]));
213        }
214
215        Ok(())
216    }
217
218    // ─────────────────────────────────────────────────────────────────────────
219    // Core recursive schema compiler
220    // ─────────────────────────────────────────────────────────────────────────
221
222    /// Compile one JSON Schema node at `depth`.  Returns the NT id whose rules
223    /// represent the language defined by the schema.
224    fn compile_schema(
225        &mut self,
226        schema: &Value,
227        depth: usize,
228    ) -> Result<NonTerminalId, JsonSchemaCompileError> {
229        if depth > MAX_DEPTH {
230            return Err(JsonSchemaCompileError::DepthExceeded { limit: MAX_DEPTH });
231        }
232
233        // Handle non-object schemas (booleans: `true` → any, `false` → nothing).
234        // JSON Schema allows `true` / `false` as schemas; we treat them as any
235        // or empty respectively, though in practice this is rare in our use-case.
236        if schema.is_boolean() {
237            // Treat `true` as empty-string match (epsilon) — accept anything
238            // by returning an NT that produces epsilon.  This is a pragmatic
239            // simplification for the constrained-decoding use case.
240            let nt = self.grammar.alloc_nt("__bool_schema");
241            self.grammar
242                .add_rule(Rule::new(nt, vec![Symbol::Terminal(vec![])]));
243            return Ok(nt);
244        }
245
246        let obj = match schema.as_object() {
247            Some(o) => o,
248            None => {
249                return Err(JsonSchemaCompileError::InvalidSchema(
250                    "schema must be a JSON object or boolean".to_string(),
251                ));
252            }
253        };
254
255        // ── Reject unsupported top-level keywords ────────────────────────────
256        for unsupported in &[
257            "not",
258            "if",
259            "then",
260            "else",
261            "patternProperties",
262            "pattern",
263            "format",
264            "multipleOf",
265            "exclusiveMinimum",
266            "exclusiveMaximum",
267        ] {
268            if obj.contains_key(*unsupported) {
269                return Err(JsonSchemaCompileError::UnsupportedKeyword(
270                    unsupported.to_string(),
271                ));
272            }
273        }
274
275        // ── $ref ─────────────────────────────────────────────────────────────
276        if let Some(ref_val) = obj.get("$ref") {
277            return self.compile_ref(ref_val);
278        }
279
280        // ── enum ─────────────────────────────────────────────────────────────
281        if let Some(enum_val) = obj.get("enum") {
282            return self.compile_enum(enum_val, depth);
283        }
284
285        // ── anyOf ────────────────────────────────────────────────────────────
286        if let Some(any_of) = obj.get("anyOf") {
287            return self.compile_any_of(any_of, depth);
288        }
289
290        // ── oneOf (identical handling to anyOf) ──────────────────────────────
291        if let Some(one_of) = obj.get("oneOf") {
292            return self.compile_any_of(one_of, depth);
293        }
294
295        // ── allOf ────────────────────────────────────────────────────────────
296        if let Some(all_of) = obj.get("allOf") {
297            return self.compile_all_of(all_of, schema, depth);
298        }
299
300        // ── type-dispatched ──────────────────────────────────────────────────
301        match obj.get("type").and_then(|v| v.as_str()) {
302            Some("string") => self.compile_string_type(),
303            Some("integer") => self.compile_integer_type(),
304            Some("number") => self.compile_number_type(),
305            Some("boolean") => Ok(self.compile_boolean_type()),
306            Some("null") => Ok(self.compile_null_type()),
307            Some("object") => self.compile_object_type(schema, depth),
308            Some("array") => self.compile_array_type(schema, depth),
309            Some(other) => Err(JsonSchemaCompileError::InvalidSchema(format!(
310                "unknown type: '{other}'"
311            ))),
312            None => {
313                // Schema without "type": if it has no other composition
314                // keywords, treat it as the "value" NT (any JSON value).
315                // This handles `{}` (empty schema) gracefully.
316                Ok(self.compile_any_value_type())
317            }
318        }
319    }
320
321    // ─────────────────────────────────────────────────────────────────────────
322    // $ref
323    // ─────────────────────────────────────────────────────────────────────────
324
325    fn compile_ref(&mut self, ref_val: &Value) -> Result<NonTerminalId, JsonSchemaCompileError> {
326        let ref_str = ref_val.as_str().ok_or_else(|| {
327            JsonSchemaCompileError::InvalidSchema("$ref must be a string".to_string())
328        })?;
329
330        // We support: "#/$defs/Foo" and "#/definitions/Foo".
331        let key = if let Some(k) = ref_str.strip_prefix("#/$defs/") {
332            k
333        } else if let Some(k) = ref_str.strip_prefix("#/definitions/") {
334            k
335        } else {
336            return Err(JsonSchemaCompileError::UnsupportedKeyword(format!(
337                "$ref to external schema or unsupported path: {ref_str}"
338            )));
339        };
340
341        self.defs_nt
342            .get(key)
343            .copied()
344            .ok_or_else(|| JsonSchemaCompileError::DanglingRef(ref_str.to_string()))
345    }
346
347    // ─────────────────────────────────────────────────────────────────────────
348    // enum
349    // ─────────────────────────────────────────────────────────────────────────
350
351    fn compile_enum(
352        &mut self,
353        enum_val: &Value,
354        _depth: usize,
355    ) -> Result<NonTerminalId, JsonSchemaCompileError> {
356        let values = enum_val.as_array().ok_or_else(|| {
357            JsonSchemaCompileError::InvalidSchema("\"enum\" value must be a JSON array".to_string())
358        })?;
359
360        if values.is_empty() {
361            return Err(JsonSchemaCompileError::InvalidSchema(
362                "\"enum\" array must not be empty".to_string(),
363            ));
364        }
365
366        let enum_nt = self.grammar.alloc_nt("__enum");
367
368        for v in values {
369            let literal = json_value_to_literal(v)?;
370            // Each alternative: enum_nt ::= <literal bytes>
371            self.grammar
372                .add_rule(Rule::new(enum_nt, vec![Symbol::Terminal(literal)]));
373        }
374
375        Ok(enum_nt)
376    }
377
378    // ─────────────────────────────────────────────────────────────────────────
379    // anyOf / oneOf
380    // ─────────────────────────────────────────────────────────────────────────
381
382    fn compile_any_of(
383        &mut self,
384        arr: &Value,
385        depth: usize,
386    ) -> Result<NonTerminalId, JsonSchemaCompileError> {
387        let variants = arr.as_array().ok_or_else(|| {
388            JsonSchemaCompileError::InvalidSchema("anyOf/oneOf must be an array".to_string())
389        })?;
390
391        if variants.is_empty() {
392            return Err(JsonSchemaCompileError::InvalidSchema(
393                "anyOf/oneOf must have at least one variant".to_string(),
394            ));
395        }
396
397        let any_nt = self.grammar.alloc_nt("__anyOf");
398
399        for variant in variants {
400            let var_nt = self.compile_schema(variant, depth + 1)?;
401            // any_nt ::= <variant_nt>
402            self.grammar
403                .add_rule(Rule::new(any_nt, vec![Symbol::NonTerminal(var_nt)]));
404        }
405
406        Ok(any_nt)
407    }
408
409    // ─────────────────────────────────────────────────────────────────────────
410    // allOf
411    // ─────────────────────────────────────────────────────────────────────────
412
413    /// Compile `allOf` by merging all branches if they are all object schemas.
414    /// Otherwise returns `UnsupportedKeyword`.
415    fn compile_all_of(
416        &mut self,
417        all_of_arr: &Value,
418        _parent: &Value,
419        depth: usize,
420    ) -> Result<NonTerminalId, JsonSchemaCompileError> {
421        let variants = all_of_arr.as_array().ok_or_else(|| {
422            JsonSchemaCompileError::InvalidSchema("allOf must be an array".to_string())
423        })?;
424
425        if variants.is_empty() {
426            return Err(JsonSchemaCompileError::InvalidSchema(
427                "allOf must have at least one element".to_string(),
428            ));
429        }
430
431        // Verify all variants are object schemas (have "type":"object" or just
432        // have "properties").
433        for variant in variants {
434            let is_object = variant
435                .get("type")
436                .and_then(|v| v.as_str())
437                .map(|t| t == "object")
438                .unwrap_or(false)
439                || variant.get("properties").is_some();
440
441            if !is_object {
442                return Err(JsonSchemaCompileError::UnsupportedKeyword(
443                    "allOf can only merge object schemas in this compiler".to_string(),
444                ));
445            }
446        }
447
448        // Merge all properties and required fields.
449        let mut merged_props: serde_json::Map<String, Value> = serde_json::Map::new();
450        let mut merged_required: Vec<String> = Vec::new();
451        let mut has_additional_false = false;
452
453        for variant in variants {
454            if let Some(props) = variant.get("properties").and_then(|v| v.as_object()) {
455                for (k, v) in props {
456                    merged_props.insert(k.clone(), v.clone());
457                }
458            }
459            if let Some(req) = variant.get("required").and_then(|v| v.as_array()) {
460                for r in req {
461                    if let Some(s) = r.as_str() {
462                        if !merged_required.contains(&s.to_string()) {
463                            merged_required.push(s.to_string());
464                        }
465                    }
466                }
467            }
468            if variant
469                .get("additionalProperties")
470                .and_then(|v| v.as_bool())
471                == Some(false)
472            {
473                has_additional_false = true;
474            }
475        }
476
477        // Build a merged object schema and compile it.
478        let mut merged = serde_json::json!({
479            "type": "object",
480            "properties": merged_props,
481            "required": merged_required,
482        });
483        if has_additional_false {
484            if let Some(obj) = merged.as_object_mut() {
485                obj.insert("additionalProperties".to_string(), Value::Bool(false));
486            }
487        }
488
489        self.compile_object_type(&merged, depth)
490    }
491
492    // ─────────────────────────────────────────────────────────────────────────
493    // Primitive types
494    // ─────────────────────────────────────────────────────────────────────────
495
496    /// Returns the shared string NT, allocating it on first use.
497    ///
498    /// Grammar fragment (simplified — allows printable ASCII except `"` and `\`):
499    ///
500    /// ```text
501    /// <string>      ::= '"' '"' | '"' <string_chars> '"'
502    /// <string_chars> ::= <string_char> | <string_char> <string_chars>
503    /// <string_char>  ::= 0x20 | 0x21 | 0x23..=0x5B | 0x5D..=0x7E
504    /// ```
505    fn compile_string_type(&mut self) -> Result<NonTerminalId, JsonSchemaCompileError> {
506        if let Some(nt) = self.string_nt {
507            return Ok(nt);
508        }
509
510        let str_nt = self.grammar.alloc_nt("__string");
511        let chars_nt = self.grammar.alloc_nt("__string_chars");
512        let char_nt = self.grammar.alloc_nt("__string_char");
513
514        // __string ::= '"' '"'
515        self.grammar.add_rule(Rule::new(
516            str_nt,
517            vec![Symbol::Terminal(vec![b'"']), Symbol::Terminal(vec![b'"'])],
518        ));
519        // __string ::= '"' __string_chars '"'
520        self.grammar.add_rule(Rule::new(
521            str_nt,
522            vec![
523                Symbol::Terminal(vec![b'"']),
524                Symbol::NonTerminal(chars_nt),
525                Symbol::Terminal(vec![b'"']),
526            ],
527        ));
528
529        // __string_chars ::= __string_char
530        self.grammar
531            .add_rule(Rule::new(chars_nt, vec![Symbol::NonTerminal(char_nt)]));
532        // __string_chars ::= __string_char __string_chars
533        self.grammar.add_rule(Rule::new(
534            chars_nt,
535            vec![Symbol::NonTerminal(char_nt), Symbol::NonTerminal(chars_nt)],
536        ));
537
538        // __string_char ::= 0x20 | 0x21 | 0x23..=0x5B | 0x5D..=0x7E
539        // (printable ASCII excluding '"' (0x22) and '\' (0x5C))
540        for b in 0x20u8..=0x21u8 {
541            self.grammar
542                .add_rule(Rule::new(char_nt, vec![Symbol::Terminal(vec![b])]));
543        }
544        for b in 0x23u8..=0x5Bu8 {
545            self.grammar
546                .add_rule(Rule::new(char_nt, vec![Symbol::Terminal(vec![b])]));
547        }
548        for b in 0x5Du8..=0x7Eu8 {
549            self.grammar
550                .add_rule(Rule::new(char_nt, vec![Symbol::Terminal(vec![b])]));
551        }
552
553        self.string_nt = Some(str_nt);
554        Ok(str_nt)
555    }
556
557    /// Returns the shared digit NT (0–9), allocating on first use.
558    fn ensure_digit_nt(&mut self) -> NonTerminalId {
559        if let Some(nt) = self.digit_nt {
560            return nt;
561        }
562        let digit_nt = self.grammar.alloc_nt("__digit");
563        for b in b'0'..=b'9' {
564            self.grammar
565                .add_rule(Rule::new(digit_nt, vec![Symbol::Terminal(vec![b])]));
566        }
567        self.digit_nt = Some(digit_nt);
568        digit_nt
569    }
570
571    /// Compile `{"type":"integer"}`.
572    ///
573    /// Grammar:
574    /// ```text
575    /// <integer>      ::= <digits> | '-' <digits>
576    /// <digits>       ::= <digit> | <digit> <digits>
577    /// <digit>        ::= '0' | ... | '9'
578    /// ```
579    fn compile_integer_type(&mut self) -> Result<NonTerminalId, JsonSchemaCompileError> {
580        let digit_nt = self.ensure_digit_nt();
581
582        let digits_nt = self.grammar.alloc_nt("__digits");
583        // __digits ::= __digit
584        self.grammar
585            .add_rule(Rule::new(digits_nt, vec![Symbol::NonTerminal(digit_nt)]));
586        // __digits ::= __digit __digits
587        self.grammar.add_rule(Rule::new(
588            digits_nt,
589            vec![
590                Symbol::NonTerminal(digit_nt),
591                Symbol::NonTerminal(digits_nt),
592            ],
593        ));
594
595        let int_nt = self.grammar.alloc_nt("__integer");
596        // __integer ::= __digits
597        self.grammar
598            .add_rule(Rule::new(int_nt, vec![Symbol::NonTerminal(digits_nt)]));
599        // __integer ::= '-' __digits
600        self.grammar.add_rule(Rule::new(
601            int_nt,
602            vec![Symbol::Terminal(vec![b'-']), Symbol::NonTerminal(digits_nt)],
603        ));
604
605        Ok(int_nt)
606    }
607
608    /// Compile `{"type":"number"}`.
609    ///
610    /// Grammar:
611    /// ```text
612    /// <number>    ::= <integer> | <integer> '.' <digits>
613    /// ```
614    fn compile_number_type(&mut self) -> Result<NonTerminalId, JsonSchemaCompileError> {
615        let int_nt = self.compile_integer_type()?;
616        // Reuse the `__digits` NT if already allocated; allocate a fresh one
617        // for the fractional part to avoid ambiguity with integer's own digits NT.
618        let digit_nt = self.ensure_digit_nt();
619
620        let frac_digits_nt = self.grammar.alloc_nt("__frac_digits");
621        // __frac_digits ::= __digit
622        self.grammar.add_rule(Rule::new(
623            frac_digits_nt,
624            vec![Symbol::NonTerminal(digit_nt)],
625        ));
626        // __frac_digits ::= __digit __frac_digits
627        self.grammar.add_rule(Rule::new(
628            frac_digits_nt,
629            vec![
630                Symbol::NonTerminal(digit_nt),
631                Symbol::NonTerminal(frac_digits_nt),
632            ],
633        ));
634
635        let num_nt = self.grammar.alloc_nt("__number");
636        // __number ::= __integer
637        self.grammar
638            .add_rule(Rule::new(num_nt, vec![Symbol::NonTerminal(int_nt)]));
639        // __number ::= __integer '.' __frac_digits
640        self.grammar.add_rule(Rule::new(
641            num_nt,
642            vec![
643                Symbol::NonTerminal(int_nt),
644                Symbol::Terminal(vec![b'.']),
645                Symbol::NonTerminal(frac_digits_nt),
646            ],
647        ));
648
649        Ok(num_nt)
650    }
651
652    /// Compile `{"type":"boolean"}`.  Returns an NT with two rules: `true` and `false`.
653    fn compile_boolean_type(&mut self) -> NonTerminalId {
654        let bool_nt = self.grammar.alloc_nt("__boolean");
655        self.grammar
656            .add_rule(Rule::new(bool_nt, vec![Symbol::Terminal(b"true".to_vec())]));
657        self.grammar.add_rule(Rule::new(
658            bool_nt,
659            vec![Symbol::Terminal(b"false".to_vec())],
660        ));
661        bool_nt
662    }
663
664    /// Compile `{"type":"null"}`.  Returns an NT with one rule: `null`.
665    fn compile_null_type(&mut self) -> NonTerminalId {
666        let null_nt = self.grammar.alloc_nt("__null");
667        self.grammar
668            .add_rule(Rule::new(null_nt, vec![Symbol::Terminal(b"null".to_vec())]));
669        null_nt
670    }
671
672    /// Compile a schema with no "type" (or the `{}` schema) as a generic JSON
673    /// value: string | integer | number | boolean | null | object | array.
674    ///
675    /// This is deliberately non-recursive for the array/object arms to avoid
676    /// infinite grammar expansion; we emit shallow stubs that match valid JSON
677    /// structure (empty object `{}` and empty array `[]`).
678    fn compile_any_value_type(&mut self) -> NonTerminalId {
679        let val_nt = self.grammar.alloc_nt("__any_value");
680
681        // Primitives
682        let str_nt = self.string_nt.unwrap_or_else(|| {
683            // Lazily compile; ignore error (primitives always succeed).
684            self.compile_string_type().expect("string type compile")
685        });
686        let bool_nt = self.compile_boolean_type();
687        let null_nt = self.compile_null_type();
688
689        // For integer and number, allocate digits NT first.
690        let digit_nt = self.ensure_digit_nt();
691        let digits_nt = self.grammar.alloc_nt("__any_val_digits");
692        self.grammar
693            .add_rule(Rule::new(digits_nt, vec![Symbol::NonTerminal(digit_nt)]));
694        self.grammar.add_rule(Rule::new(
695            digits_nt,
696            vec![
697                Symbol::NonTerminal(digit_nt),
698                Symbol::NonTerminal(digits_nt),
699            ],
700        ));
701
702        let num_nt = self.grammar.alloc_nt("__any_val_num");
703        self.grammar
704            .add_rule(Rule::new(num_nt, vec![Symbol::NonTerminal(digits_nt)]));
705        self.grammar.add_rule(Rule::new(
706            num_nt,
707            vec![Symbol::Terminal(vec![b'-']), Symbol::NonTerminal(digits_nt)],
708        ));
709
710        // Empty object stub: `{}`
711        let obj_stub_nt = self.grammar.alloc_nt("__any_val_obj");
712        self.grammar.add_rule(Rule::new(
713            obj_stub_nt,
714            vec![Symbol::Terminal(vec![b'{']), Symbol::Terminal(vec![b'}'])],
715        ));
716
717        // Empty array stub: `[]`
718        let arr_stub_nt = self.grammar.alloc_nt("__any_val_arr");
719        self.grammar.add_rule(Rule::new(
720            arr_stub_nt,
721            vec![Symbol::Terminal(vec![b'[']), Symbol::Terminal(vec![b']'])],
722        ));
723
724        // __any_value ::= __string | __boolean | __null | __any_val_num | {} | []
725        self.grammar
726            .add_rule(Rule::new(val_nt, vec![Symbol::NonTerminal(str_nt)]));
727        self.grammar
728            .add_rule(Rule::new(val_nt, vec![Symbol::NonTerminal(bool_nt)]));
729        self.grammar
730            .add_rule(Rule::new(val_nt, vec![Symbol::NonTerminal(null_nt)]));
731        self.grammar
732            .add_rule(Rule::new(val_nt, vec![Symbol::NonTerminal(num_nt)]));
733        self.grammar
734            .add_rule(Rule::new(val_nt, vec![Symbol::NonTerminal(obj_stub_nt)]));
735        self.grammar
736            .add_rule(Rule::new(val_nt, vec![Symbol::NonTerminal(arr_stub_nt)]));
737
738        val_nt
739    }
740
741    // ─────────────────────────────────────────────────────────────────────────
742    // object
743    // ─────────────────────────────────────────────────────────────────────────
744
745    /// Compile `{"type":"object","properties":{...},"required":[...]}`.
746    ///
747    /// In v1, only required properties are included in the grammar body.
748    /// Optional properties are silently omitted when `additionalProperties: false`.
749    ///
750    /// The grammar shape (for required props `[p1, p2]`) is:
751    /// ```text
752    /// <obj> ::= '{' '"p1"' ':' <S1> ',' '"p2"' ':' <S2> '}'
753    ///         | '{' '}'        ← only when there are no required properties
754    /// ```
755    fn compile_object_type(
756        &mut self,
757        schema: &Value,
758        depth: usize,
759    ) -> Result<NonTerminalId, JsonSchemaCompileError> {
760        // Reject additionalProperties: <subschema> (only false is allowed).
761        if let Some(ap) = schema.get("additionalProperties") {
762            if !ap.is_boolean() {
763                return Err(JsonSchemaCompileError::UnsupportedKeyword(
764                    "additionalProperties as a subschema is not supported; use false or omit it"
765                        .to_string(),
766                ));
767            }
768        }
769
770        let properties = schema
771            .get("properties")
772            .and_then(|v| v.as_object())
773            .cloned()
774            .unwrap_or_default();
775
776        let required: Vec<String> = schema
777            .get("required")
778            .and_then(|v| v.as_array())
779            .map(|arr| {
780                arr.iter()
781                    .filter_map(|v| v.as_str().map(|s| s.to_string()))
782                    .collect()
783            })
784            .unwrap_or_default();
785
786        // Only compile required properties in v1.
787        let obj_nt = self.grammar.alloc_nt("__object");
788
789        if required.is_empty() {
790            // Empty object: `{}`
791            self.grammar.add_rule(Rule::new(
792                obj_nt,
793                vec![Symbol::Terminal(vec![b'{']), Symbol::Terminal(vec![b'}'])],
794            ));
795            return Ok(obj_nt);
796        }
797
798        // Compile the value NT for each required property.
799        // We must compile these before building the rule body to avoid
800        // borrow issues with self.grammar.
801        let mut prop_nts: Vec<(String, NonTerminalId)> = Vec::new();
802        for prop_name in &required {
803            let sub_schema = properties.get(prop_name).ok_or_else(|| {
804                JsonSchemaCompileError::InvalidSchema(format!(
805                    "required property '{prop_name}' not found in 'properties'"
806                ))
807            })?;
808            let val_nt = self.compile_schema(sub_schema, depth + 1)?;
809            prop_nts.push((prop_name.clone(), val_nt));
810        }
811
812        // Build the rule body:
813        // '{' key0 ':' <val0> ',' key1 ':' <val1> ... '}'
814        let mut body: Vec<Symbol> = Vec::new();
815        body.push(Symbol::Terminal(vec![b'{']));
816
817        for (i, (prop_name, val_nt)) in prop_nts.iter().enumerate() {
818            if i > 0 {
819                body.push(Symbol::Terminal(vec![b',']));
820            }
821            // Property key as a JSON string literal: '"propname"'
822            let key_bytes = json_string_literal_bytes(prop_name);
823            body.push(Symbol::Terminal(key_bytes));
824            body.push(Symbol::Terminal(vec![b':']));
825            body.push(Symbol::NonTerminal(*val_nt));
826        }
827
828        body.push(Symbol::Terminal(vec![b'}']));
829        self.grammar.add_rule(Rule::new(obj_nt, body));
830
831        Ok(obj_nt)
832    }
833
834    // ─────────────────────────────────────────────────────────────────────────
835    // array
836    // ─────────────────────────────────────────────────────────────────────────
837
838    /// Compile `{"type":"array","items":<S>}`.
839    ///
840    /// Grammar:
841    /// ```text
842    /// <array>       ::= '[' ']'
843    ///                 | '[' <array_items> ']'
844    /// <array_items> ::= <S_nt>
845    ///                 | <S_nt> ',' <array_items>
846    /// ```
847    fn compile_array_type(
848        &mut self,
849        schema: &Value,
850        depth: usize,
851    ) -> Result<NonTerminalId, JsonSchemaCompileError> {
852        let items_schema = schema.get("items");
853
854        let item_nt = if let Some(items) = items_schema {
855            self.compile_schema(items, depth + 1)?
856        } else {
857            // No items schema — use the generic value NT.
858            self.compile_any_value_type()
859        };
860
861        let items_nt = self.grammar.alloc_nt("__array_items");
862        // __array_items ::= <item_nt>
863        self.grammar
864            .add_rule(Rule::new(items_nt, vec![Symbol::NonTerminal(item_nt)]));
865        // __array_items ::= <item_nt> ',' __array_items
866        self.grammar.add_rule(Rule::new(
867            items_nt,
868            vec![
869                Symbol::NonTerminal(item_nt),
870                Symbol::Terminal(vec![b',']),
871                Symbol::NonTerminal(items_nt),
872            ],
873        ));
874
875        let arr_nt = self.grammar.alloc_nt("__array");
876        // __array ::= '[' ']'
877        self.grammar.add_rule(Rule::new(
878            arr_nt,
879            vec![Symbol::Terminal(vec![b'[']), Symbol::Terminal(vec![b']'])],
880        ));
881        // __array ::= '[' __array_items ']'
882        self.grammar.add_rule(Rule::new(
883            arr_nt,
884            vec![
885                Symbol::Terminal(vec![b'[']),
886                Symbol::NonTerminal(items_nt),
887                Symbol::Terminal(vec![b']']),
888            ],
889        ));
890
891        Ok(arr_nt)
892    }
893}
894
895// ─────────────────────────────────────────────────────────────────────────────
896// Utility helpers
897// ─────────────────────────────────────────────────────────────────────────────
898
899/// Convert a `serde_json::Value` to its JSON literal byte representation,
900/// suitable for a single terminal symbol in the grammar.
901///
902/// Supports: strings, integers, booleans, null.
903/// Returns `InvalidSchema` for floats or arrays/objects (not valid enum members
904/// in our simplified v1).
905fn json_value_to_literal(v: &Value) -> Result<Vec<u8>, JsonSchemaCompileError> {
906    match v {
907        Value::String(_) => {
908            // Emit as JSON string literal: `"<escaped>"`
909            let json_repr = serde_json::to_string(v)
910                .map_err(|e| JsonSchemaCompileError::InvalidSchema(e.to_string()))?;
911            Ok(json_repr.into_bytes())
912        }
913        Value::Number(n) => {
914            // Integers only in v1.
915            if n.is_i64() || n.is_u64() {
916                Ok(n.to_string().into_bytes())
917            } else {
918                Err(JsonSchemaCompileError::UnsupportedKeyword(
919                    "float enum values are not supported".to_string(),
920                ))
921            }
922        }
923        Value::Bool(b) => {
924            if *b {
925                Ok(b"true".to_vec())
926            } else {
927                Ok(b"false".to_vec())
928            }
929        }
930        Value::Null => Ok(b"null".to_vec()),
931        _ => Err(JsonSchemaCompileError::UnsupportedKeyword(
932            "enum values must be strings, integers, booleans, or null".to_string(),
933        )),
934    }
935}
936
937/// Build the byte sequence for a JSON string literal `"<key>"`.
938/// The key is assumed to be a plain ASCII identifier (no escaping needed for
939/// typical JSON Schema property names).  For safety we still pass through
940/// `serde_json::to_string` so that non-ASCII / special characters are escaped.
941fn json_string_literal_bytes(key: &str) -> Vec<u8> {
942    serde_json::to_string(key)
943        .expect("key serialization must succeed")
944        .into_bytes()
945}
946
947// ─────────────────────────────────────────────────────────────────────────────
948// Internal unit tests
949// ─────────────────────────────────────────────────────────────────────────────
950
951#[cfg(test)]
952mod tests {
953    use super::*;
954
955    #[test]
956    fn compile_string_gives_grammar() {
957        let g = compile_json_schema_str(r#"{"type":"string"}"#).expect("should compile");
958        assert!(!g.rules.is_empty());
959    }
960
961    #[test]
962    fn compile_boolean_gives_grammar() {
963        let g = compile_json_schema_str(r#"{"type":"boolean"}"#).expect("should compile");
964        assert!(!g.rules.is_empty());
965    }
966
967    #[test]
968    fn json_value_to_literal_string() {
969        let v = Value::String("hello".to_string());
970        let b = json_value_to_literal(&v).expect("ok");
971        assert_eq!(b, br#""hello""#);
972    }
973
974    #[test]
975    fn json_value_to_literal_integer() {
976        let v = Value::Number(serde_json::Number::from(42));
977        let b = json_value_to_literal(&v).expect("ok");
978        assert_eq!(b, b"42");
979    }
980
981    #[test]
982    fn json_value_to_literal_bool() {
983        let b = json_value_to_literal(&Value::Bool(true)).expect("ok");
984        assert_eq!(b, b"true");
985    }
986
987    #[test]
988    fn json_value_to_literal_null() {
989        let b = json_value_to_literal(&Value::Null).expect("ok");
990        assert_eq!(b, b"null");
991    }
992
993    #[test]
994    fn json_string_literal_bytes_simple() {
995        let b = json_string_literal_bytes("name");
996        assert_eq!(b, br#""name""#);
997    }
998}