Skip to main content

maskforge_core/
ir.rs

1//! The public arena schema IR: a flat, immutable, hash-consed DAG built once through [`Builder`]
2//! and consumed directly by the compiler. All fields are private; the only ways in are
3//! [`Builder::finish`] and [`SchemaIR::from_wire`], and the only way out is a read-only accessor.
4
5use std::sync::OnceLock;
6
7use bincode::{Decode, Encode};
8use fluent_uri::Uri;
9use rustc_hash::FxHashMap;
10
11use crate::diagnostics::{Diagnostic, UnsupportedReason};
12use crate::error::{CompileError, ErrorCode, Stage};
13use crate::primitives::NodeId;
14use crate::wire::SchemaIRWire;
15
16/// The IR schema version. Checked exactly on every wire crossing and folded into the content hash.
17pub const IR_VERSION: u16 = 6;
18
19// Arena caps, enforced at BOTH construction paths: incrementally by the builder and on the decoded
20// value by the wire layer. Chosen so a within-cap arena always encodes below the envelope byte
21// limit, which keeps encoding infallible and keeps decode able to accept anything encode produces.
22pub(crate) const MAX_NODES: usize = 1 << 18;
23pub(crate) const MAX_STRINGS_BYTES: usize = 8 * 1024 * 1024;
24pub(crate) const MAX_LITERALS: usize = 1 << 18;
25/// Aggregate byte cap over all `ScalarLit::Str` payloads, which the count cap alone does not bound.
26pub(crate) const MAX_LITERAL_STR_BYTES: usize = 8 * 1024 * 1024;
27pub(crate) const MAX_PROPS: usize = 1 << 18;
28pub(crate) const MAX_DEPENDENT_REQUIRED_PAIRS: usize = 1 << 18;
29pub(crate) const MAX_REQUIRED_WORDS: usize = 1 << 18;
30pub(crate) const MAX_DIAGNOSTICS: usize = 1 << 16;
31pub(crate) const MAX_NODE_REFS: usize = 1 << 18;
32pub(crate) const MAX_RESOURCES: usize = 1 << 16;
33pub(crate) const MAX_ANCHORS: usize = 1 << 18;
34
35/// Total bytes of owned variable-length literal content (`Str` and composite `Json`) in a slice.
36/// Both are unbounded String payloads, so both count toward the aggregate byte cap.
37pub(crate) fn literal_str_bytes(lits: &[ScalarLit]) -> usize {
38    lits.iter()
39        .map(|l| match l {
40            ScalarLit::Str(s) | ScalarLit::Json(s) => s.len(),
41            _ => 0,
42        })
43        .sum()
44}
45
46/// A contiguous run in a side blob: `off` is the start index, `len` the element/field count.
47macro_rules! slice_ref {
48    ($(#[$m:meta])* $name:ident) => {
49        $(#[$m])*
50        #[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Debug, Default, Encode, Decode)]
51        pub struct $name {
52            /// Start index into the owning blob.
53            pub off: u32,
54            /// Element or field count.
55            pub len: u32,
56        }
57    };
58}
59
60slice_ref!(
61    /// A run of bytes in the `strings` blob (a UTF-8 substring).
62    StrRef
63);
64slice_ref!(
65    /// A run of `ScalarLit` values in the `literals` blob.
66    LitSlice
67);
68slice_ref!(
69    /// A run of `(name, child)` pairs in the `props` blob.
70    PropSlice
71);
72slice_ref!(
73    /// A run of `(trigger, required)` property-name pairs.
74    DependentRequiredSlice
75);
76slice_ref!(
77    /// A run of required-flag words in the `required_bits` blob: `off` is the first word, `len`
78    /// the field count.
79    BitSlice
80);
81slice_ref!(
82    /// A run of `NodeId` branches in the `node_refs` blob, in declaration order.
83    RefSlice
84);
85slice_ref!(
86    /// A run of anchor records in the resource metadata table.
87    AnchorSlice
88);
89
90/// Dense frozen schema-resource identifier.
91#[repr(transparent)]
92#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Debug, Default, Encode, Decode)]
93pub struct ResourceId(pub(crate) u32);
94
95impl ResourceId {
96    /// Returns the underlying dense index.
97    #[must_use]
98    pub const fn get(self) -> u32 {
99        self.0
100    }
101}
102
103/// Dense frozen anchor-record identifier.
104#[repr(transparent)]
105#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Debug, Default, Encode, Decode)]
106pub struct AnchorId(pub(crate) u32);
107
108impl AnchorId {
109    /// Returns the underlying dense index.
110    #[must_use]
111    pub const fn get(self) -> u32 {
112        self.0
113    }
114}
115
116/// Frozen metadata for one Draft 2020-12 schema resource.
117#[derive(Clone, PartialEq, Eq, Hash, Debug, Encode, Decode)]
118pub struct ResourceRecord {
119    /// Canonical absolute URI, absent only for an anonymous resource.
120    pub canonical_uri: Option<StrRef>,
121    /// Root node of this schema resource.
122    pub root: NodeId,
123    /// Static `$anchor` records owned by this resource.
124    pub static_anchors: AnchorSlice,
125    /// `$dynamicAnchor` records owned by this resource.
126    pub dynamic_anchors: AnchorSlice,
127}
128
129/// Frozen static or dynamic anchor declaration.
130#[derive(Clone, PartialEq, Eq, Hash, Debug, Encode, Decode)]
131pub struct AnchorRecord {
132    /// Resource declaring this anchor.
133    pub resource: ResourceId,
134    /// Plain-name anchor value.
135    pub name: StrRef,
136    /// Schema node identified by the anchor.
137    pub target: NodeId,
138}
139
140/// Builder input for one already-resolved resource. Anchor lists must be name-sorted.
141pub(crate) struct ResourceInput {
142    pub(crate) canonical_uri: Option<String>,
143    pub(crate) root: NodeId,
144    pub(crate) static_anchors: Vec<(String, NodeId)>,
145    pub(crate) dynamic_anchors: Vec<(String, NodeId)>,
146}
147
148/// A literal usable as a `const` value or an `enum` member.
149#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Debug, Encode, Decode)]
150pub enum ScalarLit {
151    /// JSON `null`.
152    Null,
153    /// JSON `true`/`false`.
154    Bool(bool),
155    /// A signed 64-bit integer.
156    Int(i64),
157    /// A UTF-8 string.
158    Str(String),
159    /// A composite value held as its canonical JSON serialization (minimal whitespace, declared key
160    /// order); only that one spelling is accepted (see PROVENANCE.md).
161    Json(String),
162}
163
164/// The byte alphabet a bounded string is drawn from. The discriminant is the stable wire/hash
165/// code; variants are append-only (a reorder is a wire break and bumps the envelope format).
166#[derive(Copy, Clone, PartialEq, Eq, Hash, Debug, Encode, Decode)]
167pub enum Charset {
168    /// Any UTF-8 body (no length enforcement in this profile).
169    Utf8Any = 0,
170    /// Printable ASCII excluding `"` and `\`, where one code point equals one byte.
171    AsciiPrintableNoQuoteBackslash = 1,
172    /// Full JSON-string alphabet where `regex` denotes one code point, so `{min,max}` counts them.
173    Utf8CountedCodepoints = 2,
174}
175
176/// How an object treats properties not named in `fields`. Discriminant is the stable code.
177#[derive(Copy, Clone, PartialEq, Eq, Hash, Debug, Encode, Decode)]
178pub enum ObjectClosure {
179    /// An open object (absent or non-`false` additional properties); rejected at compile.
180    RejectOpenObjects = 0,
181    /// The caller opted in to generating as a closed object.
182    AssumeClosedProfile = 1,
183    /// `additionalProperties: false` stated by the schema itself.
184    Forbidden = 2,
185    /// Default: absent `additionalProperties` compiles as truly open (spec-correct `additionalProperties:true`), not rejected.
186    AllowOpenProfile = 3,
187}
188
189/// How an open object treats keys past `known`/`patterns`.
190#[derive(Copy, Clone, PartialEq, Eq, Hash, Debug, Encode, Decode)]
191pub enum AdditionalPolicy {
192    /// No further keys are allowed.
193    Forbid,
194    /// Any further key with any JSON value is allowed.
195    AllowAny,
196    /// Any further key is allowed if its value matches this schema.
197    Schema(NodeId),
198    /// Allows an unevaluated key to flow to the unevaluated check.
199    /// It validates like `AllowAny` but differs in annotation collection.
200    Open,
201}
202
203/// Which instance type a `Node::Unevaluated` obligation targets.
204#[derive(Copy, Clone, PartialEq, Eq, Hash, Debug, Encode, Decode)]
205pub enum UnevaluatedKind {
206    /// `unevaluatedProperties`: obligation applies to object members.
207    Properties,
208    /// `unevaluatedItems`: obligation applies to array elements.
209    Items,
210}
211
212/// What an array's elements must satisfy. Unlike `AdditionalPolicy`, there is no `Forbid`: an array
213/// always accepts elements, only their type may be unconstrained.
214#[derive(Copy, Clone, PartialEq, Eq, Hash, Debug, Encode, Decode)]
215pub enum ItemsPolicy {
216    /// Every element must match this schema.
217    Schema(NodeId),
218    /// Any JSON value, arbitrary depth - not a regular language, so this always routes through
219    /// `crate::structured`.
220    AllowAny,
221}
222
223/// What `contains` requires an element to be, independent of `items`/`prefixItems`.
224#[derive(Copy, Clone, PartialEq, Eq, Hash, Debug, Encode, Decode)]
225pub enum ContainsPolicy {
226    /// `contains: false` - no element can ever satisfy it.
227    Never,
228    /// `contains: true` - every element satisfies it.
229    Always,
230    /// An element satisfies it by matching this schema.
231    Schema(NodeId),
232}
233
234/// A `contains` assertion: `min..=max` elements (independent of `items`/`prefixItems`) must
235/// satisfy `policy`. Not a regular-language property, so this always needs `crate::structured`.
236#[derive(Copy, Clone, PartialEq, Eq, Hash, Debug, Encode, Decode)]
237pub struct ContainsConstraint {
238    /// What counts as a match.
239    pub policy: ContainsPolicy,
240    /// Inclusive minimum number of matching elements.
241    pub min: u32,
242    /// Inclusive maximum number of matching elements, when bounded.
243    pub max: Option<u32>,
244}
245
246/// What the compiler does when an occurrence is `Unsupported`. Discriminant is the stable code.
247#[derive(Copy, Clone, PartialEq, Eq, Hash, Debug, Encode, Decode)]
248pub enum UnsupportedPolicy {
249    /// Any `Unsupported` occurrence fails the compile with its diagnostics.
250    RejectAtCompile = 0,
251}
252
253/// Options that govern lowering. Language-affecting choices (`object_closure`, `format_assertion`)
254/// are resolved into node fields at build time; `max_diagnostics` bounds the diagnostics buffer.
255#[derive(Clone, PartialEq, Eq, Hash, Debug, Encode, Decode)]
256pub struct CompileOptions {
257    /// The default closure applied to an object with absent additional properties.
258    pub object_closure: ObjectClosure,
259    /// Whether `format` keywords are enforced as lexical DFAs.
260    pub format_assertion: bool,
261    /// The policy for `Unsupported` occurrences.
262    pub unsupported_policy: UnsupportedPolicy,
263    /// The diagnostics buffer cap; must be at least one.
264    pub max_diagnostics: u32,
265}
266
267impl Default for CompileOptions {
268    fn default() -> Self {
269        Self {
270            object_closure: ObjectClosure::AllowOpenProfile,
271            format_assertion: false,
272            unsupported_policy: UnsupportedPolicy::RejectAtCompile,
273            max_diagnostics: 8192,
274        }
275    }
276}
277
278/// Above this many fields, a closed object's shuffle-into-one-regex can take unbounded time to even
279/// DISCOVER it exceeds `automaton::elimination`'s byte cap (subset explosion over optional fields),
280/// so both `requires_structured_backend` and `structured::MatchCache` treat it as a pre-check, never
281/// a "try it and see" attempt.
282pub(crate) const MAX_SAFE_SHUFFLE_FIELDS: usize = 4;
283/// Above this, a counted-codepoint string routes to `crate::structured`'s O(1)-per-char counter
284/// instead of an unrolled DFA whose state count is O(max_len).
285pub(crate) const MAX_UNROLLED_CODEPOINT_LEN: u32 = 256;
286/// Above this `maxItems`, an array routes to `crate::structured`'s native element counter instead of
287/// unrolling the element DFA `max_items` times.
288pub(crate) const MAX_UNROLLED_ARRAY_ITEMS: u32 = 64;
289/// Above this member count, an all-string enum routes to `crate::structured`'s O(log N) binary
290/// search instead of a giant alternation DFA (which state-explodes and crashes past ~10k).
291pub(crate) const MAX_UNROLLED_ENUM: usize = 128;
292/// One arena node. Children are `NodeId` indices; scalars live in side blobs. `#[non_exhaustive]`.
293#[non_exhaustive]
294#[derive(Clone, PartialEq, Eq, Hash, Debug, Encode, Decode)]
295pub enum Node {
296    /// Matches literal `null`.
297    Null,
298    /// Matches `true` or `false`.
299    Boolean,
300    /// Matches no value at all (the bare `false` schema). A regular language like any other leaf:
301    /// an impossible byte class, dead from the very first byte, never needs the structured backend.
302    Never,
303    /// A fixed string literal.
304    StringConst {
305        /// The literal value (a JSON string body, before JSON quoting).
306        value: StrRef,
307    },
308    /// A regular pattern over a chosen byte alphabet, optionally length-bounded.
309    StringPattern {
310        /// The byte-level regex body.
311        regex: StrRef,
312        /// Inclusive minimum length in code points, when bounded.
313        min_len: Option<u32>,
314        /// Inclusive maximum length in code points, when bounded.
315        max_len: Option<u32>,
316        /// The alphabet the body is drawn from.
317        charset: Charset,
318    },
319    /// A signed 64-bit integer with optional inclusive bounds and an optional exact divisor.
320    Integer {
321        /// Inclusive lower bound.
322        minimum: Option<i64>,
323        /// Inclusive upper bound.
324        maximum: Option<i64>,
325        /// `multipleOf`: the value must be exactly divisible by this positive integer.
326        multiple_of: Option<u64>,
327    },
328    /// Any JSON number with sign, integer part, and optional fraction.
329    /// Bounded forms omit exponents; genuinely fractional bounds remain unsupported.
330    Number {
331        /// Whether the mathematical value, rather than its spelling, must be integral.
332        integer_only: bool,
333        /// Lower bound as `(scaled_value, scale, exclusive)`: the value is `scaled_value * 10^-scale`
334        /// (`scale = 0` is an integer bound), `exclusive = true` for a strict bound.
335        minimum: Option<(i128, u32, bool)>,
336        /// Upper bound, same `(scaled_value, scale, exclusive)` encoding.
337        maximum: Option<(i128, u32, bool)>,
338        /// `multipleOf` as `coef * 10^-exp` (exact decimal), when present.
339        multiple_of: Option<(u64, u32)>,
340    },
341    /// A JSON number constrained by lexical digit counts.
342    LexicalNumber {
343        /// Anchored byte regex for the complete number token.
344        regex: StrRef,
345    },
346    /// A closed set of scalar literals (non-empty, canonically sorted, duplicate-free).
347    Enum {
348        /// The member values.
349        values: LitSlice,
350    },
351    /// A homogeneous array with a cardinality bound.
352    Array {
353        /// What every element must satisfy.
354        items: ItemsPolicy,
355        /// Inclusive minimum item count.
356        min_items: u32,
357        /// Inclusive maximum item count, when bounded.
358        max_items: Option<u32>,
359        /// `uniqueItems`: no two items may be the same JSON value. Not a regular-language property
360        /// (needs unbounded memory of prior items), so this always routes through `crate::structured`.
361        unique_items: bool,
362        /// `contains`/`minContains`/`maxContains`, when present.
363        contains: Option<ContainsConstraint>,
364        /// Whether `items` was textually present (`true` or a schema), for `unevaluatedItems`: an
365        /// absent `items` validates identically but annotates no index (proven by the official suite).
366        items_annotates: bool,
367    },
368    /// A positional (tuple) array: `prefix` positions validate in order, `tail` validates the rest
369    /// (absent = closed); length runs from `min_items` to `max_items` (and the prefix, if closed).
370    Tuple {
371        /// The positional element schemas, in array order.
372        prefix: RefSlice,
373        /// The schema for items past the prefix, or `None` for a closed tuple.
374        tail: Option<NodeId>,
375        /// Inclusive minimum item count.
376        min_items: u32,
377        /// Inclusive maximum item count, when bounded.
378        max_items: Option<u32>,
379        /// `uniqueItems`, same semantics and structured-backend requirement as `Node::Array`'s.
380        unique_items: bool,
381        /// Whether `items` was textually present past `prefixItems`, mirroring `items_annotates`.
382        tail_annotates: bool,
383        /// `contains`/`minContains`/`maxContains`, when present.
384        contains: Option<ContainsConstraint>,
385    },
386    /// An object with source-ordered fields, a positional required bitset, and a closure policy.
387    Object {
388        /// The `(name, child)` fields, in source order.
389        fields: PropSlice,
390        /// The required-field bitset (indexed by field position).
391        required: BitSlice,
392        /// How additional properties are treated.
393        closure: ObjectClosure,
394        /// `dependentSchemas`: when `key` is present, the whole instance must also satisfy
395        /// `schema`. Instance-dependent, so a non-empty run always needs `crate::structured`.
396        dependent: PropSlice,
397        /// `dependentRequired` flattened as decoded trigger/required name pairs.
398        dependent_required: DependentRequiredSlice,
399    },
400    /// An order-independent object: known properties, `patternProperties`, and an additional-key
401    /// policy, none of which can be represented as one finite byte-level regex.
402    OpenObject {
403        /// The exact `(name, child)` properties, matched independent of textual order.
404        known: PropSlice,
405        /// The required-field bitset, aligned to `known` by position.
406        known_required: BitSlice,
407        /// The `(pattern, child)` `patternProperties` entries; a key may match more than one.
408        patterns: PropSlice,
409        /// How a key matching neither `known` nor `patterns` is treated.
410        additional: AdditionalPolicy,
411        /// An optional schema every key (known, pattern-matched, or additional) must satisfy.
412        property_names: Option<NodeId>,
413        /// Inclusive minimum property count.
414        min_properties: Option<u32>,
415        /// Inclusive maximum property count, when bounded.
416        max_properties: Option<u32>,
417        /// `dependentSchemas`, same semantics as `Node::Object`'s.
418        dependent: PropSlice,
419        /// `dependentRequired` flattened as decoded trigger/required name pairs.
420        dependent_required: DependentRequiredSlice,
421    },
422    /// A `type` union: matches any of its branches, in declaration order (deduplicated by type).
423    Union {
424        /// The branch nodes.
425        branches: RefSlice,
426    },
427    /// An `allOf`: matches iff every branch matches (language intersection).
428    Intersection {
429        /// The branch nodes.
430        branches: RefSlice,
431    },
432    /// A `oneOf`: matches iff exactly one branch matches.
433    ExactlyOne {
434        /// The branch nodes.
435        branches: RefSlice,
436    },
437    /// Matches complete JSON values that `inner` does not match.
438    /// Full JSON-value complement requires `crate::structured`.
439    Not {
440        /// The negated schema.
441        inner: NodeId,
442    },
443    /// A recursion cut point: a leaf whose body is `def_targets[def]`, an edge free of the back-edge
444    /// rule so a `$ref` cycle needs no cyclic child graph. Followed only by `crate::structured`.
445    Ref {
446        /// Index into the `SchemaIR::def_targets` table naming the definition body.
447        def: u32,
448    },
449    /// A `$dynamicRef` whose initially resolved fragment names a `$dynamicAnchor`.
450    DynamicRef {
451        /// Statically resolved target used when dynamic scope does not rebind the name.
452        initial_target: NodeId,
453        /// Initial dynamic-anchor declaration, which supplies the plain-name identity.
454        anchor: AnchorId,
455    },
456    /// Requires unannotated properties or items in `scope` to match `unevaluated`.
457    /// Annotation tracking requires `crate::structured`.
458    Unevaluated {
459        /// Which instance type this obligation targets.
460        kind: UnevaluatedKind,
461        /// The schema minus the unevaluated keyword (local assertions and in-place applicators).
462        scope: NodeId,
463        /// The schema each unevaluated member must satisfy (`Never` for `false`).
464        unevaluated: NodeId,
465    },
466    /// An occurrence the dialect does not support. Carries no pointer; provenance is the sidecar.
467    Unsupported {
468        /// The offending keyword.
469        keyword: StrRef,
470        /// The machine-stable reason.
471        reason: UnsupportedReason,
472    },
473}
474
475/// The arena IR. Built once and frozen; all fields private.
476#[derive(Clone)]
477pub struct SchemaIR {
478    ir_version: u16,
479    root: NodeId,
480    nodes: Vec<Node>,
481    strings: Vec<u8>,
482    literals: Vec<ScalarLit>,
483    props: Vec<(StrRef, NodeId)>,
484    dependent_required_pairs: Vec<DependentRequiredPair>,
485    required_bits: Vec<u64>,
486    node_refs: Vec<NodeId>,
487    def_targets: Vec<NodeId>,
488    retrieval_uri: Option<StrRef>,
489    resources: Vec<ResourceRecord>,
490    anchors: Vec<AnchorRecord>,
491    node_resources: Vec<ResourceId>,
492    options: CompileOptions,
493    diagnostics: Vec<Diagnostic>,
494    cached_hash: OnceLock<[u8; 32]>,
495    route_table: OnceLock<crate::routing::RouteTable>,
496}
497
498/// One decoded-name presence implication used by `dependentRequired`.
499#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Debug, Encode, Decode)]
500pub struct DependentRequiredPair {
501    /// The decoded property name whose presence activates the rule.
502    pub trigger: StrRef,
503    /// A decoded property name that must be present when the trigger is present.
504    pub required: StrRef,
505}
506
507impl PartialEq for SchemaIR {
508    /// Exact frozen representation equality: every field (arena order, blob offsets, options, and
509    /// diagnostics) except the memoized hash. Two IRs with the same schema but a different arena
510    /// layout or different rejected sites compare unequal; for language identity use
511    /// [`SchemaIR::same_content_address`] or compare [`SchemaIR::canonical_hash`].
512    fn eq(&self, other: &Self) -> bool {
513        self.ir_version == other.ir_version
514            && self.root == other.root
515            && self.nodes == other.nodes
516            && self.strings == other.strings
517            && self.literals == other.literals
518            && self.props == other.props
519            && self.dependent_required_pairs == other.dependent_required_pairs
520            && self.required_bits == other.required_bits
521            && self.node_refs == other.node_refs
522            && self.def_targets == other.def_targets
523            && self.retrieval_uri == other.retrieval_uri
524            && self.resources == other.resources
525            && self.anchors == other.anchors
526            && self.node_resources == other.node_resources
527            && self.options == other.options
528            && self.diagnostics == other.diagnostics
529    }
530}
531
532impl Eq for SchemaIR {}
533
534impl std::fmt::Debug for SchemaIR {
535    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
536        f.debug_struct("SchemaIR")
537            .field("ir_version", &self.ir_version)
538            .field("root", &self.root)
539            .field("nodes", &self.nodes)
540            .field("resources", &self.resources)
541            .field("diagnostics", &self.diagnostics.len())
542            .finish_non_exhaustive()
543    }
544}
545
546impl SchemaIR {
547    /// The IR schema version.
548    #[must_use]
549    pub fn ir_version(&self) -> u16 {
550        self.ir_version
551    }
552
553    /// The root node id.
554    #[must_use]
555    pub fn root(&self) -> NodeId {
556        self.root
557    }
558
559    /// The node at `id`, or `None` if the id is out of range.
560    #[must_use]
561    pub fn node(&self, id: NodeId) -> Option<&Node> {
562        self.nodes.get(id.get() as usize)
563    }
564
565    /// All nodes in arena order.
566    pub fn nodes(&self) -> impl ExactSizeIterator<Item = &Node> {
567        self.nodes.iter()
568    }
569
570    /// The number of nodes in the arena.
571    #[must_use]
572    pub fn node_count(&self) -> usize {
573        self.nodes.len()
574    }
575
576    /// Optional root retrieval URI retained in the compact string blob.
577    #[must_use]
578    pub fn retrieval_uri(&self) -> Option<&str> {
579        self.retrieval_uri.and_then(|uri| self.str_at(uri))
580    }
581
582    /// Frozen schema resources in canonical order.
583    #[must_use]
584    pub fn resources(&self) -> &[ResourceRecord] {
585        &self.resources
586    }
587
588    /// Frozen static and dynamic anchor records.
589    #[must_use]
590    pub fn anchors(&self) -> &[AnchorRecord] {
591        &self.anchors
592    }
593
594    /// Resource that owns `node`, or `None` for an invalid node ID.
595    #[must_use]
596    pub fn node_resource(&self, node: NodeId) -> Option<ResourceId> {
597        self.node_resources.get(node.get() as usize).copied()
598    }
599
600    /// Every diagnostic, one per rejected occurrence.
601    pub fn diagnostics(&self) -> impl ExactSizeIterator<Item = &Diagnostic> {
602        self.diagnostics.iter()
603    }
604
605    /// A counted-codepoint string whose bound is past `MAX_UNROLLED_CODEPOINT_LEN`, and whose regex
606    /// is exactly `JSON_STRING_CODEPOINT` (the only text the frontend ever pairs with this charset).
607    pub(crate) fn is_unrolled_codepoint_string(&self, node: &Node) -> bool {
608        let Node::StringPattern {
609            regex,
610            min_len,
611            max_len,
612            charset: Charset::Utf8CountedCodepoints,
613        } = node
614        else {
615            return false;
616        };
617        let long = max_len.is_some_and(|n| n > MAX_UNROLLED_CODEPOINT_LEN)
618            || min_len.is_some_and(|n| n > MAX_UNROLLED_CODEPOINT_LEN);
619        long && self.str_at(*regex) == Some(crate::frontend::JSON_STRING_CODEPOINT)
620    }
621
622    /// A `pattern`/`format` string, as opposed to the unconstrained-body sentinel: its regex must
623    /// run over decoded Unicode scalar bytes (`crate::structured`), never raw JSON escape spelling.
624    pub(crate) fn is_user_pattern(&self, node: &Node) -> bool {
625        let Node::StringPattern {
626            regex,
627            charset: Charset::Utf8Any,
628            ..
629        } = node
630        else {
631            return false;
632        };
633        self.str_at(*regex) != Some(crate::frontend::JSON_STRING_BODY)
634    }
635
636    /// An enum past `MAX_UNROLLED_ENUM` whose members are all strings: compiled as a decoded-value
637    /// trie when regular, or matched by the structured backend inside non-regular composition.
638    pub(crate) fn is_large_string_enum(&self, node: &Node) -> bool {
639        let Node::Enum { values } = node else {
640            return false;
641        };
642        let Some(lits) = self.lits_at(*values) else {
643            return false;
644        };
645        lits.len() > MAX_UNROLLED_ENUM && lits.iter().all(|l| matches!(l, ScalarLit::Str(_)))
646    }
647
648    /// Whether any arena node needs `crate::structured` instead of `compile::compile_ir`: an
649    /// `OpenObject`, an oversized object shuffle, `uniqueItems`/`contains`, `dependentSchemas`, or an
650    /// unrolled-codepoint-count string too long to build as one DFA cheaply.
651    #[must_use]
652    pub fn requires_structured_backend(&self) -> bool {
653        self.route_table()
654            .get(self.root)
655            .is_none_or(|route| route.kind != crate::routing::ExecutionKind::Regular)
656    }
657
658    /// The shared immutable per-node route table, computed once for this frozen IR.
659    #[must_use]
660    pub fn route_table(&self) -> &crate::routing::RouteTable {
661        self.route_table
662            .get_or_init(|| crate::routing::analyze(self))
663    }
664
665    /// The compile options this IR was built with.
666    #[must_use]
667    pub fn options(&self) -> &CompileOptions {
668        &self.options
669    }
670
671    /// Resolves a string reference to its UTF-8 slice, or `None` if the run is out of range.
672    #[must_use]
673    pub fn str_at(&self, r: StrRef) -> Option<&str> {
674        let end = (r.off as usize).checked_add(r.len as usize)?;
675        let bytes = self.strings.get(r.off as usize..end)?;
676        std::str::from_utf8(bytes).ok()
677    }
678
679    /// Resolves a literal run, or `None` if out of range.
680    #[must_use]
681    pub fn lits_at(&self, r: LitSlice) -> Option<&[ScalarLit]> {
682        let end = (r.off as usize).checked_add(r.len as usize)?;
683        self.literals.get(r.off as usize..end)
684    }
685
686    /// Resolves a property run, or `None` if out of range.
687    #[must_use]
688    pub fn props_at(&self, r: PropSlice) -> Option<&[(StrRef, NodeId)]> {
689        let end = (r.off as usize).checked_add(r.len as usize)?;
690        self.props.get(r.off as usize..end)
691    }
692
693    /// Resolves a `dependentRequired` pair run, or `None` if out of range.
694    #[must_use]
695    pub fn dependent_required_at(
696        &self,
697        r: DependentRequiredSlice,
698    ) -> Option<&[DependentRequiredPair]> {
699        let end = (r.off as usize).checked_add(r.len as usize)?;
700        self.dependent_required_pairs.get(r.off as usize..end)
701    }
702
703    /// The definition body a `Node::Ref { def }` names, or `None` if the slot is out of range.
704    #[must_use]
705    pub fn def_target(&self, def: u32) -> Option<NodeId> {
706        self.def_targets.get(def as usize).copied()
707    }
708
709    /// Resolves a branch run, or `None` if out of range.
710    #[must_use]
711    pub fn refs_at(&self, r: RefSlice) -> Option<&[NodeId]> {
712        let end = (r.off as usize).checked_add(r.len as usize)?;
713        self.node_refs.get(r.off as usize..end)
714    }
715
716    /// Whether field `idx` of an object with required set `r` is required.
717    #[must_use]
718    pub fn is_required(&self, r: BitSlice, idx: u32) -> bool {
719        if idx >= r.len {
720            return false;
721        }
722        let word = r.off as usize + (idx / 64) as usize;
723        self.required_bits
724            .get(word)
725            .is_some_and(|w| (w >> (idx % 64)) & 1 == 1)
726    }
727
728    /// The memoized blake3 Merkle content address over node semantics (tags, scalars, child digests,
729    /// closure, required bits) only; pointers, layout, diagnostics, and `CompileOptions` are excluded.
730    #[must_use]
731    pub fn canonical_hash(&self) -> [u8; 32] {
732        *self.cached_hash.get_or_init(|| self.compute_hash())
733    }
734
735    /// Whether two IRs share the same content address. This is cryptographic identity, not a
736    /// structural proof: equal addresses mean the schemas are the same up to a BLAKE3 collision,
737    /// which is the same guarantee any content-addressed store relies on.
738    #[must_use]
739    pub fn same_content_address(&self, other: &Self) -> bool {
740        self.canonical_hash() == other.canonical_hash()
741    }
742
743    /// Serializes the IR into the versioned MFIR envelope.
744    #[must_use]
745    pub fn to_wire(&self) -> Vec<u8> {
746        crate::wire::encode(self)
747    }
748
749    /// Deserializes and validates an MFIR envelope into a frozen IR.
750    pub fn from_wire(bytes: &[u8]) -> Result<Self, CompileError> {
751        crate::wire::decode(bytes)
752    }
753
754    /// Builds the wire payload struct (a clone of the semantic fields, without the memo).
755    pub(crate) fn to_wire_struct(&self) -> SchemaIRWire {
756        SchemaIRWire {
757            ir_version: self.ir_version,
758            root: self.root,
759            nodes: self.nodes.clone(),
760            strings: self.strings.clone(),
761            literals: self.literals.clone(),
762            props: self.props.clone(),
763            dependent_required_pairs: self.dependent_required_pairs.clone(),
764            required_bits: self.required_bits.clone(),
765            node_refs: self.node_refs.clone(),
766            def_targets: self.def_targets.clone(),
767            retrieval_uri: self.retrieval_uri,
768            resources: self.resources.clone(),
769            anchors: self.anchors.clone(),
770            node_resources: self.node_resources.clone(),
771            options: self.options.clone(),
772            diagnostics: self.diagnostics.clone(),
773        }
774    }
775
776    /// The single construction choke point: validate the parts, then freeze with an empty memo.
777    /// Panic-free on adversarial input (used by both the builder and the wire decoder).
778    pub(crate) fn assemble(wire: SchemaIRWire) -> Result<Self, CompileError> {
779        let ir = Self {
780            ir_version: wire.ir_version,
781            root: wire.root,
782            nodes: wire.nodes,
783            strings: wire.strings,
784            literals: wire.literals,
785            props: wire.props,
786            dependent_required_pairs: wire.dependent_required_pairs,
787            required_bits: wire.required_bits,
788            node_refs: wire.node_refs,
789            def_targets: wire.def_targets,
790            retrieval_uri: wire.retrieval_uri,
791            resources: wire.resources,
792            anchors: wire.anchors,
793            node_resources: wire.node_resources,
794            options: wire.options,
795            diagnostics: wire.diagnostics,
796            cached_hash: OnceLock::new(),
797            route_table: OnceLock::new(),
798        };
799        ir.validate()?;
800        Ok(ir)
801    }
802
803    /// Structural validation. Total and panic-free: every index is checked, every child is a
804    /// strict back-edge, every set is canonical. Rejects a forged arena with a structured error.
805    fn validate(&self) -> Result<(), CompileError> {
806        if self.ir_version != IR_VERSION {
807            return Err(err(
808                ErrorCode::ArtifactMismatch,
809                "ir_version does not match",
810            ));
811        }
812        if self.options.max_diagnostics == 0 {
813            return Err(malformed("max_diagnostics must be at least one"));
814        }
815        if self.root.get() as usize >= self.nodes.len() {
816            return Err(malformed("root node id out of range"));
817        }
818        if literal_str_bytes(&self.literals) > MAX_LITERAL_STR_BYTES {
819            return Err(limit("literal string bytes"));
820        }
821        if self.dependent_required_pairs.len() > MAX_DEPENDENT_REQUIRED_PAIRS {
822            return Err(malformed("dependentRequired pair count exceeds cap"));
823        }
824        self.validate_resources()?;
825        for (i, node) in self.nodes.iter().enumerate() {
826            let parent = u32::try_from(i).map_err(|_| limit("node index"))?;
827            self.validate_node(node, parent)?;
828        }
829        for target in &self.def_targets {
830            if target.get() as usize >= self.nodes.len() {
831                return Err(malformed("def target node id out of range"));
832            }
833        }
834        self.validate_diagnostics()?;
835        Ok(())
836    }
837
838    /// Re-runs structural validation for benchmark phase attribution.
839    #[doc(hidden)]
840    #[cfg(feature = "bench-internals")]
841    pub fn validate_for_bench(&self) -> Result<(), CompileError> {
842        self.validate()
843    }
844
845    fn validate_diagnostics(&self) -> Result<(), CompileError> {
846        if self.diagnostics.len() as u64 > u64::from(self.options.max_diagnostics) {
847            return Err(CompileError::new(
848                ErrorCode::TooManyDiagnostics,
849                Stage::L2,
850                "more diagnostics than the configured cap",
851            ));
852        }
853        for d in &self.diagnostics {
854            let node = self
855                .nodes
856                .get(d.node.get() as usize)
857                .ok_or_else(|| malformed("diagnostic references a node out of range"))?;
858            let Node::Unsupported { keyword, reason } = node else {
859                return Err(malformed("a diagnostic must point at an Unsupported node"));
860            };
861            if *keyword != d.keyword {
862                return Err(malformed("diagnostic keyword disagrees with its node"));
863            }
864            if *reason != d.reason {
865                return Err(malformed("diagnostic reason disagrees with its node"));
866            }
867            if self.str_at(d.keyword).is_none() || self.str_at(d.json_pointer).is_none() {
868                return Err(malformed("diagnostic string reference out of range"));
869            }
870            if let Some(span) = d.source_span {
871                if span.start > span.end {
872                    return Err(malformed("diagnostic span start is past its end"));
873                }
874            }
875        }
876        Ok(())
877    }
878
879    fn validate_node(&self, node: &Node, parent: u32) -> Result<(), CompileError> {
880        let child_ok = |c: NodeId| c.get() < parent;
881        match node {
882            Node::Null | Node::Boolean | Node::Never => Ok(()),
883            Node::StringConst { value } => {
884                self.str_at(*value)
885                    .ok_or_else(|| malformed("string const reference out of range"))?;
886                Ok(())
887            }
888            Node::StringPattern {
889                regex,
890                min_len,
891                max_len,
892                charset,
893            } => {
894                let re = self
895                    .str_at(*regex)
896                    .ok_or_else(|| malformed("pattern reference out of range"))?;
897                if *charset == Charset::Utf8Any && re != crate::frontend::JSON_STRING_BODY {
898                    validate_search_pattern(re)?;
899                } else {
900                    validate_pattern(re)?;
901                }
902                if let (Some(lo), Some(hi)) = (min_len, max_len) {
903                    if hi < lo {
904                        return Err(malformed("string maxLength is less than minLength"));
905                    }
906                }
907                Ok(())
908            }
909            Node::Integer {
910                minimum,
911                maximum,
912                multiple_of,
913            } => {
914                if let (Some(lo), Some(hi)) = (minimum, maximum) {
915                    if hi < lo {
916                        return Err(malformed("integer maximum is less than minimum"));
917                    }
918                }
919                if *multiple_of == Some(0) {
920                    return Err(malformed("multipleOf must be a positive integer"));
921                }
922                Ok(())
923            }
924            Node::Number {
925                integer_only: _,
926                minimum,
927                maximum,
928                multiple_of,
929            } => {
930                if let (Some((lo, ls, lo_excl)), Some((hi, hs, hi_excl))) = (minimum, maximum) {
931                    let ord = decimal_cmp((*lo, *ls), (*hi, *hs));
932                    if ord == std::cmp::Ordering::Greater
933                        || (ord == std::cmp::Ordering::Equal && (*lo_excl || *hi_excl))
934                    {
935                        return Err(malformed("number maximum is not above minimum"));
936                    }
937                }
938                if let Some((coef, _)) = multiple_of {
939                    if *coef == 0 {
940                        return Err(malformed("multipleOf must be positive"));
941                    }
942                }
943                Ok(())
944            }
945            Node::LexicalNumber { regex } => {
946                let regex = self
947                    .str_at(*regex)
948                    .ok_or_else(|| malformed("lexical number pattern out of range"))?;
949                validate_pattern(regex)
950            }
951            Node::Enum { values } => {
952                let lits = self
953                    .lits_at(*values)
954                    .ok_or_else(|| malformed("enum literal run out of range"))?;
955                if lits.is_empty() {
956                    return Err(malformed("an enum must have at least one value"));
957                }
958                for pair in lits.windows(2) {
959                    if pair[0] >= pair[1] {
960                        return Err(malformed(
961                            "enum values must be canonically sorted and unique",
962                        ));
963                    }
964                }
965                Ok(())
966            }
967            Node::Array {
968                items,
969                min_items,
970                max_items,
971                ..
972            } => {
973                if let ItemsPolicy::Schema(id) = items {
974                    if !child_ok(*id) {
975                        return Err(malformed("array item is not a back-edge"));
976                    }
977                }
978                if let Some(max) = max_items {
979                    if max < min_items {
980                        return Err(malformed("array maxItems is less than minItems"));
981                    }
982                }
983                Ok(())
984            }
985            Node::Tuple {
986                prefix,
987                tail,
988                min_items,
989                max_items,
990                ..
991            } => {
992                let ids = self
993                    .refs_at(*prefix)
994                    .ok_or_else(|| malformed("tuple prefix run out of range"))?;
995                if ids.is_empty() {
996                    return Err(malformed("a tuple must have at least one prefix position"));
997                }
998                for id in ids {
999                    if !child_ok(*id) {
1000                        return Err(malformed("tuple prefix element is not a back-edge"));
1001                    }
1002                }
1003                if let Some(t) = tail {
1004                    if !child_ok(*t) {
1005                        return Err(malformed("tuple tail is not a back-edge"));
1006                    }
1007                }
1008                if let Some(max) = max_items {
1009                    if max < min_items {
1010                        return Err(malformed("tuple maxItems is less than minItems"));
1011                    }
1012                }
1013                // A closed tuple (no tail) cannot hold more items than the prefix supplies, so a
1014                // minItems past the prefix length is an empty language, rejected at build.
1015                if tail.is_none() && *min_items > prefix.len {
1016                    return Err(malformed("closed tuple minItems exceeds the prefix length"));
1017                }
1018                Ok(())
1019            }
1020            Node::Object {
1021                fields,
1022                required,
1023                dependent,
1024                dependent_required,
1025                ..
1026            } => {
1027                let props = self
1028                    .props_at(*fields)
1029                    .ok_or_else(|| malformed("object field run out of range"))?;
1030                let mut seen: FxHashMap<&str, ()> = FxHashMap::default();
1031                seen.reserve(props.len());
1032                for (name, child) in props {
1033                    let key = self
1034                        .str_at(*name)
1035                        .ok_or_else(|| malformed("object field name out of range"))?;
1036                    if seen.insert(key, ()).is_some() {
1037                        return Err(malformed("duplicate object key"));
1038                    }
1039                    if !child_ok(*child) {
1040                        return Err(malformed("object field value is not a back-edge"));
1041                    }
1042                }
1043                self.validate_required(*required, props.len())?;
1044                self.validate_dependent_props(*dependent, child_ok)?;
1045                self.validate_dependent_required(*dependent_required)
1046            }
1047            Node::OpenObject {
1048                known,
1049                known_required,
1050                patterns,
1051                additional,
1052                property_names,
1053                min_properties,
1054                max_properties,
1055                dependent,
1056                dependent_required,
1057            } => {
1058                let known_props = self
1059                    .props_at(*known)
1060                    .ok_or_else(|| malformed("open object known props out of range"))?;
1061                let mut seen: FxHashMap<&str, ()> = FxHashMap::default();
1062                seen.reserve(known_props.len());
1063                for (name, child) in known_props {
1064                    let key = self
1065                        .str_at(*name)
1066                        .ok_or_else(|| malformed("open object field name out of range"))?;
1067                    if seen.insert(key, ()).is_some() {
1068                        return Err(malformed("duplicate object key"));
1069                    }
1070                    if !child_ok(*child) {
1071                        return Err(malformed("open object field value is not a back-edge"));
1072                    }
1073                }
1074                self.validate_required(*known_required, known_props.len())?;
1075                let pattern_props = self
1076                    .props_at(*patterns)
1077                    .ok_or_else(|| malformed("open object pattern props out of range"))?;
1078                for (regex, child) in pattern_props {
1079                    let re = self
1080                        .str_at(*regex)
1081                        .ok_or_else(|| malformed("open object pattern regex out of range"))?;
1082                    validate_search_pattern(re)?;
1083                    if !child_ok(*child) {
1084                        return Err(malformed("open object pattern value is not a back-edge"));
1085                    }
1086                }
1087                if let AdditionalPolicy::Schema(id) = additional {
1088                    if !child_ok(*id) {
1089                        return Err(malformed(
1090                            "open object additional schema is not a back-edge",
1091                        ));
1092                    }
1093                }
1094                if let Some(pn) = property_names {
1095                    if !child_ok(*pn) {
1096                        return Err(malformed(
1097                            "open object propertyNames schema is not a back-edge",
1098                        ));
1099                    }
1100                }
1101                if let (Some(lo), Some(hi)) = (min_properties, max_properties) {
1102                    if hi < lo {
1103                        return Err(malformed(
1104                            "open object maxProperties is less than minProperties",
1105                        ));
1106                    }
1107                }
1108                self.validate_dependent_props(*dependent, child_ok)?;
1109                self.validate_dependent_required(*dependent_required)
1110            }
1111            Node::Union { branches } => {
1112                let ids = self
1113                    .refs_at(*branches)
1114                    .ok_or_else(|| malformed("union branch run out of range"))?;
1115                if ids.is_empty() {
1116                    return Err(malformed("a type union must have at least one branch"));
1117                }
1118                for id in ids {
1119                    if !child_ok(*id) {
1120                        return Err(malformed("union branch is not a back-edge"));
1121                    }
1122                }
1123                Ok(())
1124            }
1125            Node::Intersection { branches } => {
1126                let ids = self
1127                    .refs_at(*branches)
1128                    .ok_or_else(|| malformed("intersection branch run out of range"))?;
1129                if ids.is_empty() {
1130                    return Err(malformed("an intersection must have at least one branch"));
1131                }
1132                for id in ids {
1133                    if !child_ok(*id) {
1134                        return Err(malformed("intersection branch is not a back-edge"));
1135                    }
1136                }
1137                Ok(())
1138            }
1139            Node::ExactlyOne { branches } => {
1140                let ids = self
1141                    .refs_at(*branches)
1142                    .ok_or_else(|| malformed("exactly-one branch run out of range"))?;
1143                if ids.is_empty() {
1144                    return Err(malformed("an exactly-one must have at least one branch"));
1145                }
1146                for id in ids {
1147                    if !child_ok(*id) {
1148                        return Err(malformed("exactly-one branch is not a back-edge"));
1149                    }
1150                }
1151                Ok(())
1152            }
1153            Node::Not { inner } => {
1154                if !child_ok(*inner) {
1155                    return Err(malformed("not target is not a back-edge"));
1156                }
1157                Ok(())
1158            }
1159            Node::Ref { def } => {
1160                if (*def as usize) >= self.def_targets.len() {
1161                    return Err(malformed("ref names a def slot out of range"));
1162                }
1163                Ok(())
1164            }
1165            Node::DynamicRef {
1166                initial_target,
1167                anchor,
1168            } => {
1169                if initial_target.get() as usize >= self.nodes.len() {
1170                    return Err(malformed("dynamic reference target is out of range"));
1171                }
1172                let record = self
1173                    .anchors
1174                    .get(anchor.get() as usize)
1175                    .ok_or_else(|| malformed("dynamic reference anchor is out of range"))?;
1176                let resource = self
1177                    .resources
1178                    .get(record.resource.get() as usize)
1179                    .ok_or_else(|| {
1180                        malformed("dynamic reference anchor resource is out of range")
1181                    })?;
1182                let dynamic_start = resource.dynamic_anchors.off as usize;
1183                let dynamic_end = dynamic_start
1184                    .checked_add(resource.dynamic_anchors.len as usize)
1185                    .ok_or_else(|| malformed("dynamic anchor run overflows"))?;
1186                let anchor_index = anchor.get() as usize;
1187                if !(dynamic_start..dynamic_end).contains(&anchor_index) {
1188                    return Err(malformed(
1189                        "dynamic reference names an anchor that is not dynamic",
1190                    ));
1191                }
1192                let consistent = record.target == *initial_target
1193                    || matches!(
1194                        self.node(*initial_target),
1195                        Some(Node::Ref { def }) if self.def_target(*def) == Some(record.target)
1196                    );
1197                if !consistent {
1198                    return Err(malformed(
1199                        "dynamic reference target disagrees with its initial anchor",
1200                    ));
1201                }
1202                Ok(())
1203            }
1204            Node::Unevaluated {
1205                scope, unevaluated, ..
1206            } => {
1207                if !child_ok(*scope) || !child_ok(*unevaluated) {
1208                    return Err(malformed("unevaluated child is not a back-edge"));
1209                }
1210                Ok(())
1211            }
1212            Node::Unsupported { keyword, .. } => {
1213                self.str_at(*keyword)
1214                    .ok_or_else(|| malformed("unsupported keyword reference out of range"))?;
1215                Ok(())
1216            }
1217        }
1218    }
1219
1220    fn validate_resources(&self) -> Result<(), CompileError> {
1221        if self.resources.is_empty() || self.resources.len() > MAX_RESOURCES {
1222            return Err(malformed("resource table is empty or exceeds its cap"));
1223        }
1224        if self.anchors.len() > MAX_ANCHORS {
1225            return Err(malformed("anchor table exceeds its cap"));
1226        }
1227        if self.node_resources.len() != self.nodes.len() {
1228            return Err(malformed("node-to-resource table length mismatch"));
1229        }
1230        if let Some(uri) = self.retrieval_uri {
1231            let uri = self
1232                .str_at(uri)
1233                .ok_or_else(|| malformed("retrieval URI string run is invalid"))?;
1234            validate_canonical_uri(uri)?;
1235        }
1236        for resource in &self.node_resources {
1237            if resource.get() as usize >= self.resources.len() {
1238                return Err(malformed("node is assigned to an invalid resource"));
1239            }
1240        }
1241        let mut previous_uri: Option<&str> = None;
1242        let mut anonymous_seen = false;
1243        let mut anchor_cursor = 0usize;
1244        for (index, resource) in self.resources.iter().enumerate() {
1245            if resource.root.get() as usize >= self.nodes.len() {
1246                return Err(malformed("resource root is out of range"));
1247            }
1248            let resource_id =
1249                ResourceId(u32::try_from(index).map_err(|_| limit("resource index"))?);
1250            if self.node_resource(resource.root) != Some(resource_id) {
1251                return Err(malformed("resource root has an inconsistent owner"));
1252            }
1253            match resource.canonical_uri {
1254                None => {
1255                    if anonymous_seen || previous_uri.is_some() {
1256                        return Err(malformed("anonymous resource ordering is not canonical"));
1257                    }
1258                    anonymous_seen = true;
1259                }
1260                Some(uri) => {
1261                    let uri = self
1262                        .str_at(uri)
1263                        .ok_or_else(|| malformed("resource URI string run is invalid"))?;
1264                    validate_canonical_uri(uri)?;
1265                    if previous_uri.is_some_and(|previous| previous >= uri) {
1266                        return Err(malformed("resource URIs are duplicate or not canonical"));
1267                    }
1268                    previous_uri = Some(uri);
1269                }
1270            }
1271            let mut names = rustc_hash::FxHashSet::default();
1272            for run in [resource.static_anchors, resource.dynamic_anchors] {
1273                if usize::try_from(run.off).map_err(|_| malformed("anchor run offset"))?
1274                    != anchor_cursor
1275                {
1276                    return Err(malformed("anchor runs are not in canonical order"));
1277                }
1278                let anchors = self.validate_anchor_run(run, resource_id)?;
1279                anchor_cursor = anchor_cursor
1280                    .checked_add(anchors.len())
1281                    .ok_or_else(|| malformed("anchor run overflows"))?;
1282                names
1283                    .try_reserve(anchors.len())
1284                    .map_err(|_| limit("anchor validation set"))?;
1285                for anchor in anchors {
1286                    let name = self
1287                        .str_at(anchor.name)
1288                        .ok_or_else(|| malformed("anchor name string run is invalid"))?;
1289                    if !names.insert(name) {
1290                        return Err(malformed(
1291                            "static and dynamic anchors collide inside one resource",
1292                        ));
1293                    }
1294                }
1295            }
1296        }
1297        if anchor_cursor != self.anchors.len() {
1298            return Err(malformed("anchor table has unclaimed records"));
1299        }
1300        Ok(())
1301    }
1302
1303    fn validate_anchor_run(
1304        &self,
1305        run: AnchorSlice,
1306        resource: ResourceId,
1307    ) -> Result<&[AnchorRecord], CompileError> {
1308        let start = usize::try_from(run.off).map_err(|_| malformed("anchor run offset"))?;
1309        let end = start
1310            .checked_add(usize::try_from(run.len).map_err(|_| malformed("anchor run length"))?)
1311            .ok_or_else(|| malformed("anchor run overflows"))?;
1312        let anchors = self
1313            .anchors
1314            .get(start..end)
1315            .ok_or_else(|| malformed("anchor run is out of range"))?;
1316        let mut previous_name: Option<&str> = None;
1317        for anchor in anchors {
1318            if anchor.resource != resource {
1319                return Err(malformed("anchor run contains another resource"));
1320            }
1321            if anchor.target.get() as usize >= self.nodes.len()
1322                || self.node_resource(anchor.target) != Some(resource)
1323            {
1324                return Err(malformed("anchor target or resource is invalid"));
1325            }
1326            let name = self
1327                .str_at(anchor.name)
1328                .ok_or_else(|| malformed("anchor name string run is invalid"))?;
1329            if previous_name.is_some_and(|previous| previous >= name) {
1330                return Err(malformed("anchor names are duplicate or not canonical"));
1331            }
1332            previous_name = Some(name);
1333        }
1334        Ok(anchors)
1335    }
1336
1337    fn validate_required(
1338        &self,
1339        required: BitSlice,
1340        field_count: usize,
1341    ) -> Result<(), CompileError> {
1342        if required.len as usize != field_count {
1343            return Err(malformed(
1344                "required bitset width does not match field count",
1345            ));
1346        }
1347        let words = field_count.div_ceil(64);
1348        let end = (required.off as usize)
1349            .checked_add(words)
1350            .ok_or_else(|| malformed("required bitset overflows"))?;
1351        let slice = self
1352            .required_bits
1353            .get(required.off as usize..end)
1354            .ok_or_else(|| malformed("required bitset out of range"))?;
1355        // The tail bits past the field count must be zero so the words hash canonically.
1356        if field_count % 64 != 0 {
1357            if let Some(last) = slice.last() {
1358                let used = field_count % 64;
1359                if last >> used != 0 {
1360                    return Err(malformed("required bitset has non-zero padding bits"));
1361                }
1362            }
1363        }
1364        Ok(())
1365    }
1366
1367    /// Validates a `dependentSchemas` run: distinct keys, every value a strict back-edge.
1368    fn validate_dependent_props(
1369        &self,
1370        dependent: PropSlice,
1371        child_ok: impl Fn(NodeId) -> bool,
1372    ) -> Result<(), CompileError> {
1373        let props = self
1374            .props_at(dependent)
1375            .ok_or_else(|| malformed("dependentSchemas prop run out of range"))?;
1376        let mut seen: FxHashMap<&str, ()> = FxHashMap::default();
1377        seen.reserve(props.len());
1378        for (name, child) in props {
1379            let key = self
1380                .str_at(*name)
1381                .ok_or_else(|| malformed("dependentSchemas key out of range"))?;
1382            if seen.insert(key, ()).is_some() {
1383                return Err(malformed("duplicate dependentSchemas key"));
1384            }
1385            if !child_ok(*child) {
1386                return Err(malformed("dependentSchemas value is not a back-edge"));
1387            }
1388        }
1389        Ok(())
1390    }
1391
1392    fn validate_dependent_required(
1393        &self,
1394        dependent: DependentRequiredSlice,
1395    ) -> Result<(), CompileError> {
1396        let pairs = self
1397            .dependent_required_at(dependent)
1398            .ok_or_else(|| malformed("dependentRequired pair run out of range"))?;
1399        if pairs.len() > MAX_DEPENDENT_REQUIRED_PAIRS {
1400            return Err(malformed("dependentRequired pair count exceeds cap"));
1401        }
1402        let mut seen = rustc_hash::FxHashSet::default();
1403        seen.try_reserve(pairs.len())
1404            .map_err(|_| limit("dependentRequired validation set"))?;
1405        for pair in pairs {
1406            let trigger = self
1407                .str_at(pair.trigger)
1408                .ok_or_else(|| malformed("dependentRequired trigger out of range"))?;
1409            let required = self
1410                .str_at(pair.required)
1411                .ok_or_else(|| malformed("dependentRequired required name out of range"))?;
1412            if !seen.insert((trigger, required)) {
1413                return Err(malformed("duplicate dependentRequired pair"));
1414            }
1415        }
1416        Ok(())
1417    }
1418
1419    fn compute_hash(&self) -> [u8; 32] {
1420        let mut digests: Vec<[u8; 32]> = Vec::with_capacity(self.nodes.len());
1421        for node in &self.nodes {
1422            digests.push(self.node_digest(node, &digests));
1423        }
1424        let mut top = blake3::Hasher::new();
1425        top.update(b"MFIR-ir-v");
1426        top.update(&self.ir_version.to_le_bytes());
1427        top.update(&digests[self.root.get() as usize]);
1428        // A def body is reachable only through the def table, so its content is folded in by slot;
1429        // without this two recursive schemas sharing ref structure but differing bodies would collide.
1430        top.update(&(self.def_targets.len() as u64).to_le_bytes());
1431        for target in &self.def_targets {
1432            top.update(&digests[target.get() as usize]);
1433        }
1434        top.update(&[self.options.object_closure as u8]);
1435        top.update(&[u8::from(self.options.format_assertion)]);
1436        top.update(&[self.options.unsupported_policy as u8]);
1437        top.update(&self.options.max_diagnostics.to_le_bytes());
1438        match self.retrieval_uri() {
1439            None => {
1440                top.update(&[0u8]);
1441            }
1442            Some(uri) => {
1443                top.update(&[1u8]);
1444                feed_bytes(&mut top, uri.as_bytes())
1445            }
1446        };
1447        top.update(&(self.resources.len() as u64).to_le_bytes());
1448        for (index, resource) in self.resources.iter().enumerate() {
1449            match resource.canonical_uri.and_then(|uri| self.str_at(uri)) {
1450                None => {
1451                    top.update(&[0u8]);
1452                }
1453                Some(uri) => {
1454                    top.update(&[1u8]);
1455                    feed_bytes(&mut top, uri.as_bytes())
1456                }
1457            };
1458            top.update(&resource.root.get().to_le_bytes());
1459            let resource_id = ResourceId(u32::try_from(index).unwrap_or(u32::MAX));
1460            for (dynamic, run) in [
1461                (false, resource.static_anchors),
1462                (true, resource.dynamic_anchors),
1463            ] {
1464                top.update(&[u8::from(dynamic)]);
1465                let start = run.off as usize;
1466                let end = start.saturating_add(run.len as usize);
1467                let anchors = self.anchors.get(start..end).unwrap_or(&[]);
1468                top.update(&(anchors.len() as u64).to_le_bytes());
1469                for anchor in anchors {
1470                    top.update(&resource_id.get().to_le_bytes());
1471                    feed_bytes(&mut top, self.str_at(anchor.name).unwrap_or("").as_bytes());
1472                    top.update(&anchor.target.get().to_le_bytes());
1473                }
1474            }
1475        }
1476        top.update(&(self.node_resources.len() as u64).to_le_bytes());
1477        for resource in &self.node_resources {
1478            top.update(&resource.get().to_le_bytes());
1479        }
1480        *top.finalize().as_bytes()
1481    }
1482
1483    fn node_digest(&self, node: &Node, digests: &[[u8; 32]]) -> [u8; 32] {
1484        let mut h = blake3::Hasher::new();
1485        match node {
1486            Node::Null => h.update(&[0u8]),
1487            Node::Boolean => h.update(&[1u8]),
1488            Node::Never => h.update(&[16u8]),
1489            Node::StringConst { value } => {
1490                h.update(&[2u8]);
1491                feed_bytes(&mut h, self.str_at(*value).unwrap_or("").as_bytes());
1492                &mut h
1493            }
1494            Node::StringPattern {
1495                regex,
1496                min_len,
1497                max_len,
1498                charset,
1499            } => {
1500                h.update(&[3u8]);
1501                feed_bytes(&mut h, self.str_at(*regex).unwrap_or("").as_bytes());
1502                feed_opt_u32(&mut h, *min_len);
1503                feed_opt_u32(&mut h, *max_len);
1504                h.update(&[charset_tag(*charset)]);
1505                &mut h
1506            }
1507            Node::Integer {
1508                minimum,
1509                maximum,
1510                multiple_of,
1511            } => {
1512                h.update(&[4u8]);
1513                feed_opt_i64(&mut h, *minimum);
1514                feed_opt_i64(&mut h, *maximum);
1515                feed_opt_u64(&mut h, *multiple_of);
1516                &mut h
1517            }
1518            Node::Number {
1519                integer_only,
1520                minimum,
1521                maximum,
1522                multiple_of,
1523            } => {
1524                h.update(&[12u8]);
1525                h.update(&[u8::from(*integer_only)]);
1526                feed_opt_decimal_bound(&mut h, *minimum);
1527                feed_opt_decimal_bound(&mut h, *maximum);
1528                match multiple_of {
1529                    None => h.update(&[0u8]),
1530                    Some((coef, exp)) => h
1531                        .update(&[1u8])
1532                        .update(&coef.to_le_bytes())
1533                        .update(&exp.to_le_bytes()),
1534                };
1535                &mut h
1536            }
1537            Node::LexicalNumber { regex } => {
1538                h.update(&[17u8]);
1539                feed_bytes(&mut h, self.str_at(*regex).unwrap_or("").as_bytes());
1540                &mut h
1541            }
1542            Node::Enum { values } => {
1543                h.update(&[5u8]);
1544                let lits = self.lits_at(*values).unwrap_or(&[]);
1545                h.update(&(lits.len() as u64).to_le_bytes());
1546                for lit in lits {
1547                    feed_bytes(&mut h, &lit_bytes(lit));
1548                }
1549                &mut h
1550            }
1551            Node::Array {
1552                items,
1553                min_items,
1554                max_items,
1555                unique_items,
1556                contains,
1557                items_annotates,
1558            } => {
1559                h.update(&[6u8]);
1560                match items {
1561                    ItemsPolicy::Schema(id) => {
1562                        h.update(&[0u8]);
1563                        h.update(&digests[id.get() as usize]);
1564                    }
1565                    ItemsPolicy::AllowAny => {
1566                        h.update(&[1u8]);
1567                    }
1568                }
1569                h.update(&min_items.to_le_bytes());
1570                feed_opt_u32(&mut h, *max_items);
1571                h.update(&[u8::from(*unique_items)]);
1572                feed_contains(&mut h, contains, digests);
1573                h.update(&[u8::from(*items_annotates)]);
1574                &mut h
1575            }
1576            Node::Tuple {
1577                prefix,
1578                tail,
1579                min_items,
1580                max_items,
1581                unique_items,
1582                tail_annotates,
1583                contains,
1584            } => {
1585                h.update(&[13u8]);
1586                let ids = self.refs_at(*prefix).unwrap_or(&[]);
1587                h.update(&(ids.len() as u64).to_le_bytes());
1588                for id in ids {
1589                    h.update(&digests[id.get() as usize]);
1590                }
1591                match tail {
1592                    None => h.update(&[0u8]),
1593                    Some(t) => h.update(&[1u8]).update(&digests[t.get() as usize]),
1594                };
1595                h.update(&min_items.to_le_bytes());
1596                feed_opt_u32(&mut h, *max_items);
1597                h.update(&[u8::from(*unique_items)]);
1598                feed_contains(&mut h, contains, digests);
1599                h.update(&[u8::from(*tail_annotates)]);
1600                &mut h
1601            }
1602            Node::Object {
1603                fields,
1604                required,
1605                closure,
1606                dependent,
1607                dependent_required,
1608            } => {
1609                h.update(&[7u8]);
1610                let props = self.props_at(*fields).unwrap_or(&[]);
1611                h.update(&(props.len() as u64).to_le_bytes());
1612                // Field count is a u32 by construction, so the u32 index never overflows here.
1613                for ((name, child), idx) in props.iter().zip(0u32..) {
1614                    feed_bytes(&mut h, self.str_at(*name).unwrap_or("").as_bytes());
1615                    h.update(&digests[child.get() as usize]);
1616                    h.update(&[u8::from(self.is_required(*required, idx))]);
1617                }
1618                h.update(&[closure_tag(*closure)]);
1619                self.feed_dependent(&mut h, *dependent, digests);
1620                self.feed_dependent_required(&mut h, *dependent_required);
1621                &mut h
1622            }
1623            Node::OpenObject {
1624                known,
1625                known_required,
1626                patterns,
1627                additional,
1628                property_names,
1629                min_properties,
1630                max_properties,
1631                dependent,
1632                dependent_required,
1633            } => {
1634                h.update(&[14u8]);
1635                let kp = self.props_at(*known).unwrap_or(&[]);
1636                h.update(&(kp.len() as u64).to_le_bytes());
1637                for ((name, child), idx) in kp.iter().zip(0u32..) {
1638                    feed_bytes(&mut h, self.str_at(*name).unwrap_or("").as_bytes());
1639                    h.update(&digests[child.get() as usize]);
1640                    h.update(&[u8::from(self.is_required(*known_required, idx))]);
1641                }
1642                let pp = self.props_at(*patterns).unwrap_or(&[]);
1643                h.update(&(pp.len() as u64).to_le_bytes());
1644                for (regex, child) in pp {
1645                    feed_bytes(&mut h, self.str_at(*regex).unwrap_or("").as_bytes());
1646                    h.update(&digests[child.get() as usize]);
1647                }
1648                match additional {
1649                    AdditionalPolicy::Forbid => h.update(&[0u8]),
1650                    AdditionalPolicy::AllowAny => h.update(&[1u8]),
1651                    AdditionalPolicy::Schema(id) => {
1652                        h.update(&[2u8]).update(&digests[id.get() as usize])
1653                    }
1654                    AdditionalPolicy::Open => h.update(&[3u8]),
1655                };
1656                match property_names {
1657                    None => h.update(&[0u8]),
1658                    Some(id) => h.update(&[1u8]).update(&digests[id.get() as usize]),
1659                };
1660                feed_opt_u32(&mut h, *min_properties);
1661                feed_opt_u32(&mut h, *max_properties);
1662                self.feed_dependent(&mut h, *dependent, digests);
1663                self.feed_dependent_required(&mut h, *dependent_required);
1664                &mut h
1665            }
1666            Node::Union { branches } => {
1667                h.update(&[9u8]);
1668                let ids = self.refs_at(*branches).unwrap_or(&[]);
1669                h.update(&(ids.len() as u64).to_le_bytes());
1670                for id in ids {
1671                    h.update(&digests[id.get() as usize]);
1672                }
1673                &mut h
1674            }
1675            Node::Intersection { branches } => {
1676                h.update(&[10u8]);
1677                let ids = self.refs_at(*branches).unwrap_or(&[]);
1678                h.update(&(ids.len() as u64).to_le_bytes());
1679                for id in ids {
1680                    h.update(&digests[id.get() as usize]);
1681                }
1682                &mut h
1683            }
1684            Node::ExactlyOne { branches } => {
1685                h.update(&[11u8]);
1686                let ids = self.refs_at(*branches).unwrap_or(&[]);
1687                h.update(&(ids.len() as u64).to_le_bytes());
1688                for id in ids {
1689                    h.update(&digests[id.get() as usize]);
1690                }
1691                &mut h
1692            }
1693            Node::Not { inner } => {
1694                h.update(&[15u8]);
1695                h.update(&digests[inner.get() as usize]);
1696                &mut h
1697            }
1698            Node::Ref { def } => {
1699                h.update(&[17u8]);
1700                h.update(&def.to_le_bytes());
1701                &mut h
1702            }
1703            Node::DynamicRef {
1704                initial_target,
1705                anchor,
1706            } => {
1707                h.update(&[19u8]);
1708                h.update(&initial_target.get().to_le_bytes());
1709                h.update(&anchor.get().to_le_bytes());
1710                &mut h
1711            }
1712            Node::Unevaluated {
1713                kind,
1714                scope,
1715                unevaluated,
1716            } => {
1717                h.update(&[18u8]);
1718                h.update(&[*kind as u8]);
1719                h.update(&digests[scope.get() as usize]);
1720                h.update(&digests[unevaluated.get() as usize]);
1721                &mut h
1722            }
1723            Node::Unsupported { keyword, reason } => {
1724                h.update(&[8u8]);
1725                feed_bytes(&mut h, self.str_at(*keyword).unwrap_or("").as_bytes());
1726                h.update(&(*reason as u32).to_le_bytes());
1727                &mut h
1728            }
1729        };
1730        *h.finalize().as_bytes()
1731    }
1732
1733    /// Feeds a `dependentSchemas` run into a node digest, keyed by name so equal sets hash equal
1734    /// regardless of arena insertion order.
1735    fn feed_dependent(&self, h: &mut blake3::Hasher, dependent: PropSlice, digests: &[[u8; 32]]) {
1736        let props = self.props_at(dependent).unwrap_or(&[]);
1737        h.update(&(props.len() as u64).to_le_bytes());
1738        for (name, child) in props {
1739            feed_bytes(h, self.str_at(*name).unwrap_or("").as_bytes());
1740            h.update(&digests[child.get() as usize]);
1741        }
1742    }
1743
1744    fn feed_dependent_required(&self, h: &mut blake3::Hasher, dependent: DependentRequiredSlice) {
1745        let mut pairs: Vec<(&[u8], &[u8])> = self
1746            .dependent_required_at(dependent)
1747            .unwrap_or(&[])
1748            .iter()
1749            .map(|pair| {
1750                (
1751                    self.str_at(pair.trigger).unwrap_or("").as_bytes(),
1752                    self.str_at(pair.required).unwrap_or("").as_bytes(),
1753                )
1754            })
1755            .collect();
1756        pairs.sort_unstable();
1757        h.update(&(pairs.len() as u64).to_le_bytes());
1758        for (trigger, required) in pairs {
1759            feed_bytes(h, trigger);
1760            feed_bytes(h, required);
1761        }
1762    }
1763}
1764
1765fn feed_bytes(h: &mut blake3::Hasher, bytes: &[u8]) {
1766    h.update(&(bytes.len() as u64).to_le_bytes());
1767    h.update(bytes);
1768}
1769
1770fn feed_opt_u32(h: &mut blake3::Hasher, v: Option<u32>) {
1771    match v {
1772        None => h.update(&[0u8]),
1773        Some(x) => h.update(&[1u8]).update(&x.to_le_bytes()),
1774    };
1775}
1776
1777fn feed_opt_i64(h: &mut blake3::Hasher, v: Option<i64>) {
1778    match v {
1779        None => h.update(&[0u8]),
1780        Some(x) => h.update(&[1u8]).update(&x.to_le_bytes()),
1781    };
1782}
1783
1784fn feed_opt_decimal_bound(h: &mut blake3::Hasher, v: Option<(i128, u32, bool)>) {
1785    match v {
1786        None => h.update(&[0u8]),
1787        Some((x, scale, excl)) => h
1788            .update(&[1u8])
1789            .update(&x.to_le_bytes())
1790            .update(&scale.to_le_bytes())
1791            .update(&[u8::from(excl)]),
1792    };
1793}
1794
1795/// Compares `(coefficient, scale)` decimals without scale-sized work or overflow.
1796pub(crate) fn decimal_cmp(a: (i128, u32), b: (i128, u32)) -> std::cmp::Ordering {
1797    use std::cmp::Ordering;
1798    if a.0 == 0 || b.0 == 0 || a.0.is_negative() != b.0.is_negative() {
1799        return a.0.signum().cmp(&b.0.signum());
1800    }
1801    let (a_digits, a_len) = decimal_digits(a.0.unsigned_abs());
1802    let (b_digits, b_len) = decimal_digits(b.0.unsigned_abs());
1803    let a_exponent = i64::try_from(a_len).unwrap_or(i64::MAX) - i64::from(a.1);
1804    let b_exponent = i64::try_from(b_len).unwrap_or(i64::MAX) - i64::from(b.1);
1805    let mut magnitude = a_exponent.cmp(&b_exponent);
1806    if magnitude == Ordering::Equal {
1807        for index in 0..a_len.max(b_len) {
1808            magnitude = a_digits[index].cmp(&b_digits[index]);
1809            if magnitude != Ordering::Equal {
1810                break;
1811            }
1812        }
1813    }
1814    if a.0.is_negative() {
1815        magnitude.reverse()
1816    } else {
1817        magnitude
1818    }
1819}
1820
1821fn decimal_digits(mut value: u128) -> ([u8; 39], usize) {
1822    let mut reverse = [0u8; 39];
1823    let mut length = 0;
1824    while value > 0 {
1825        reverse[length] = u8::try_from(value % 10).unwrap_or(0);
1826        value /= 10;
1827        length += 1;
1828    }
1829    let mut digits = [0u8; 39];
1830    for index in 0..length {
1831        digits[index] = reverse[length - index - 1];
1832    }
1833    (digits, length)
1834}
1835
1836fn feed_contains(
1837    h: &mut blake3::Hasher,
1838    contains: &Option<ContainsConstraint>,
1839    digests: &[[u8; 32]],
1840) {
1841    match contains {
1842        None => {
1843            h.update(&[0u8]);
1844        }
1845        Some(c) => {
1846            h.update(&[1u8]);
1847            match c.policy {
1848                ContainsPolicy::Never => h.update(&[0u8]),
1849                ContainsPolicy::Always => h.update(&[1u8]),
1850                ContainsPolicy::Schema(id) => h.update(&[2u8]).update(&digests[id.get() as usize]),
1851            };
1852            h.update(&c.min.to_le_bytes());
1853            feed_opt_u32(h, c.max);
1854        }
1855    }
1856}
1857
1858fn feed_opt_u64(h: &mut blake3::Hasher, v: Option<u64>) {
1859    match v {
1860        None => h.update(&[0u8]),
1861        Some(x) => h.update(&[1u8]).update(&x.to_le_bytes()),
1862    };
1863}
1864
1865fn charset_tag(c: Charset) -> u8 {
1866    match c {
1867        Charset::Utf8Any => 0,
1868        Charset::AsciiPrintableNoQuoteBackslash => 1,
1869        Charset::Utf8CountedCodepoints => 2,
1870    }
1871}
1872
1873fn closure_tag(c: ObjectClosure) -> u8 {
1874    match c {
1875        ObjectClosure::RejectOpenObjects => 0,
1876        ObjectClosure::AssumeClosedProfile => 1,
1877        ObjectClosure::Forbidden => 2,
1878        ObjectClosure::AllowOpenProfile => 3,
1879    }
1880}
1881
1882fn lit_bytes(lit: &ScalarLit) -> Vec<u8> {
1883    match lit {
1884        ScalarLit::Null => vec![0u8],
1885        ScalarLit::Bool(b) => vec![1u8, u8::from(*b)],
1886        ScalarLit::Int(i) => {
1887            let mut v = vec![2u8];
1888            v.extend_from_slice(&i.to_le_bytes());
1889            v
1890        }
1891        ScalarLit::Str(s) => {
1892            let mut v = vec![3u8];
1893            v.extend_from_slice(s.as_bytes());
1894            v
1895        }
1896        ScalarLit::Json(s) => {
1897            let mut v = vec![4u8];
1898            v.extend_from_slice(s.as_bytes());
1899            v
1900        }
1901    }
1902}
1903
1904/// Rejects patterns the byte-level engine cannot compile.
1905fn validate_pattern(regex: &str) -> Result<(), CompileError> {
1906    if has_anchor_outside_brackets(regex.as_bytes()) {
1907        return Err(unsupported("explicit anchors are not allowed"));
1908    }
1909    validate_pattern_body(regex, false)
1910}
1911
1912/// Like `validate_pattern`, but permits `^` and `$` for structured search.
1913/// Used by `patternProperties` and `propertyNames`.
1914pub(crate) fn validate_search_pattern(regex: &str) -> Result<(), CompileError> {
1915    validate_pattern_body(regex, true)
1916}
1917
1918fn validate_pattern_body(regex: &str, allow_leading_assertions: bool) -> Result<(), CompileError> {
1919    use crate::frontend::regex::{analyze_compiled_pattern, PatternAnalysis};
1920
1921    match analyze_compiled_pattern(regex) {
1922        PatternAnalysis::Ordinary => Ok(()),
1923        PatternAnalysis::Leading(_) if allow_leading_assertions => Ok(()),
1924        PatternAnalysis::Leading(_) => {
1925            Err(unsupported("unsupported regex construct").with_observed("look-ahead"))
1926        }
1927        PatternAnalysis::Unsupported(what) => {
1928            Err(unsupported("unsupported regex construct").with_observed(what))
1929        }
1930        PatternAnalysis::Malformed(what) => {
1931            Err(malformed("malformed regex syntax").with_observed(what))
1932        }
1933    }
1934}
1935
1936/// True if `^` or `$` appears as an anchor (outside a bracket expression, unescaped). Inside
1937/// `[...]` those bytes are a negation or a literal, which the byte engine accepts.
1938pub(crate) fn has_anchor_outside_brackets(bytes: &[u8]) -> bool {
1939    let mut in_bracket = false;
1940    let mut i = 0;
1941    while i < bytes.len() {
1942        match bytes[i] {
1943            b'\\' => i += 1,
1944            b'[' if !in_bracket => in_bracket = true,
1945            b']' if in_bracket => in_bracket = false,
1946            b'^' | b'$' if !in_bracket => return true,
1947            _ => {}
1948        }
1949        i += 1;
1950    }
1951    false
1952}
1953
1954fn err(code: ErrorCode, message: &'static str) -> CompileError {
1955    CompileError::new(code, Stage::L2, message)
1956}
1957
1958fn malformed(message: &'static str) -> CompileError {
1959    CompileError::new(ErrorCode::Malformed, Stage::L2, message)
1960}
1961
1962fn validate_canonical_uri(value: &str) -> Result<(), CompileError> {
1963    let uri = Uri::parse(value).map_err(|_| malformed("canonical resource URI is invalid"))?;
1964    if uri.fragment().is_some() || uri.normalize().as_str() != value {
1965        return Err(malformed(
1966            "resource URI is not fragmentless canonical RFC 3986 form",
1967        ));
1968    }
1969    Ok(())
1970}
1971
1972fn unsupported(message: &'static str) -> CompileError {
1973    CompileError::new(ErrorCode::Unsupported, Stage::L2, message)
1974}
1975
1976/// Hash-consing arena builder. Structurally equal subtrees collapse to one `NodeId` because every
1977/// side blob is content-interned, so equal children yield equal `Node` values.
1978pub struct Builder {
1979    nodes: Vec<Node>,
1980    strings: Vec<u8>,
1981    literals: Vec<ScalarLit>,
1982    props: Vec<(StrRef, NodeId)>,
1983    dependent_required_pairs: Vec<DependentRequiredPair>,
1984    required_bits: Vec<u64>,
1985    node_refs: Vec<NodeId>,
1986    def_targets: Vec<Option<NodeId>>,
1987    diagnostics: Vec<Diagnostic>,
1988    literal_str_bytes: usize,
1989    node_intern: FxHashMap<(ResourceId, Node), NodeId>,
1990    node_resources: Vec<ResourceId>,
1991    current_resource: ResourceId,
1992    str_intern: FxHashMap<String, StrRef>,
1993    lit_intern: FxHashMap<Vec<ScalarLit>, LitSlice>,
1994    prop_intern: FxHashMap<Vec<(StrRef, NodeId)>, PropSlice>,
1995    dependent_required_intern: FxHashMap<Vec<DependentRequiredPair>, DependentRequiredSlice>,
1996    bit_intern: FxHashMap<Vec<u64>, BitSlice>,
1997    ref_intern: FxHashMap<Vec<NodeId>, RefSlice>,
1998    options: CompileOptions,
1999}
2000
2001impl Builder {
2002    /// A new builder with the given options.
2003    #[must_use]
2004    pub fn new(options: CompileOptions) -> Self {
2005        Self {
2006            nodes: Vec::new(),
2007            strings: Vec::new(),
2008            literals: Vec::new(),
2009            props: Vec::new(),
2010            dependent_required_pairs: Vec::new(),
2011            required_bits: Vec::new(),
2012            node_refs: Vec::new(),
2013            def_targets: Vec::new(),
2014            diagnostics: Vec::new(),
2015            literal_str_bytes: 0,
2016            node_intern: FxHashMap::default(),
2017            node_resources: Vec::new(),
2018            current_resource: ResourceId(0),
2019            str_intern: FxHashMap::default(),
2020            lit_intern: FxHashMap::default(),
2021            prop_intern: FxHashMap::default(),
2022            dependent_required_intern: FxHashMap::default(),
2023            bit_intern: FxHashMap::default(),
2024            ref_intern: FxHashMap::default(),
2025            options,
2026        }
2027    }
2028
2029    /// Interns a UTF-8 string, returning a content-addressed reference.
2030    fn intern_str(&mut self, s: &str) -> Result<StrRef, CompileError> {
2031        if let Some(r) = self.str_intern.get(s) {
2032            return Ok(*r);
2033        }
2034        if self
2035            .strings
2036            .len()
2037            .checked_add(s.len())
2038            .is_none_or(|n| n > MAX_STRINGS_BYTES)
2039        {
2040            return Err(limit("strings blob cap"));
2041        }
2042        let off = u32::try_from(self.strings.len()).map_err(|_| limit("strings blob"))?;
2043        let len = u32::try_from(s.len()).map_err(|_| limit("string length"))?;
2044        self.strings.extend_from_slice(s.as_bytes());
2045        let r = StrRef { off, len };
2046        self.str_intern.insert(s.to_owned(), r);
2047        Ok(r)
2048    }
2049
2050    fn intern_lits(&mut self, lits: Vec<ScalarLit>) -> Result<LitSlice, CompileError> {
2051        if let Some(r) = self.lit_intern.get(&lits) {
2052            return Ok(*r);
2053        }
2054        if self
2055            .literals
2056            .len()
2057            .checked_add(lits.len())
2058            .is_none_or(|n| n > MAX_LITERALS)
2059        {
2060            return Err(limit("literals cap"));
2061        }
2062        if self
2063            .literal_str_bytes
2064            .checked_add(literal_str_bytes(&lits))
2065            .is_none_or(|n| n > MAX_LITERAL_STR_BYTES)
2066        {
2067            return Err(limit("literal string bytes cap"));
2068        }
2069        self.literal_str_bytes += literal_str_bytes(&lits);
2070        let off = u32::try_from(self.literals.len()).map_err(|_| limit("literals blob"))?;
2071        let len = u32::try_from(lits.len()).map_err(|_| limit("literal run"))?;
2072        self.literals.extend_from_slice(&lits);
2073        let r = LitSlice { off, len };
2074        self.lit_intern.insert(lits, r);
2075        Ok(r)
2076    }
2077
2078    fn intern_props(&mut self, pairs: Vec<(StrRef, NodeId)>) -> Result<PropSlice, CompileError> {
2079        if let Some(r) = self.prop_intern.get(&pairs) {
2080            return Ok(*r);
2081        }
2082        if self
2083            .props
2084            .len()
2085            .checked_add(pairs.len())
2086            .is_none_or(|n| n > MAX_PROPS)
2087        {
2088            return Err(limit("props cap"));
2089        }
2090        let off = u32::try_from(self.props.len()).map_err(|_| limit("props blob"))?;
2091        let len = u32::try_from(pairs.len()).map_err(|_| limit("prop run"))?;
2092        self.props.extend_from_slice(&pairs);
2093        let r = PropSlice { off, len };
2094        self.prop_intern.insert(pairs, r);
2095        Ok(r)
2096    }
2097
2098    /// `groups` is `(trigger, required names)`: each trigger and each required name is owned
2099    /// exactly once by the caller. Interns the trigger once per group (never once per edge),
2100    /// reserves the flattened pair vector fallibly against the checked total edge count, and
2101    /// keeps the existing sorted/deduplicated/shared-slice arena representation.
2102    fn intern_dependent_required(
2103        &mut self,
2104        mut groups: Vec<(String, Vec<String>)>,
2105    ) -> Result<DependentRequiredSlice, CompileError> {
2106        let total_edges = groups
2107            .iter()
2108            .try_fold(0usize, |total, (_, required)| {
2109                total.checked_add(required.len())
2110            })
2111            .ok_or_else(|| limit("dependentRequired pair count"))?;
2112        if total_edges > MAX_DEPENDENT_REQUIRED_PAIRS {
2113            return Err(limit("dependentRequired pair count"));
2114        }
2115        groups.sort_unstable_by(|(a, _), (b, _)| a.cmp(b));
2116        if groups.windows(2).any(|pair| pair[0].0 == pair[1].0) {
2117            return Err(malformed("duplicate dependentRequired trigger"));
2118        }
2119        for (_, required) in &mut groups {
2120            required.sort_unstable();
2121            if required.windows(2).any(|pair| pair[0] == pair[1]) {
2122                return Err(malformed("duplicate dependentRequired pair"));
2123            }
2124        }
2125        let mut refs = Vec::new();
2126        refs.try_reserve_exact(total_edges)
2127            .map_err(|_| limit("dependentRequired pair arena"))?;
2128        for (trigger, required) in &groups {
2129            let trigger_ref = self.intern_str(trigger)?;
2130            for name in required {
2131                refs.push(DependentRequiredPair {
2132                    trigger: trigger_ref,
2133                    required: self.intern_str(name)?,
2134                });
2135            }
2136        }
2137        if let Some(r) = self.dependent_required_intern.get(&refs) {
2138            return Ok(*r);
2139        }
2140        if self
2141            .dependent_required_pairs
2142            .len()
2143            .checked_add(refs.len())
2144            .is_none_or(|n| n > MAX_DEPENDENT_REQUIRED_PAIRS)
2145        {
2146            return Err(limit("dependentRequired pair arena"));
2147        }
2148        let off = u32::try_from(self.dependent_required_pairs.len())
2149            .map_err(|_| limit("dependentRequired pair arena"))?;
2150        let len = u32::try_from(refs.len()).map_err(|_| limit("dependentRequired pair run"))?;
2151        self.dependent_required_pairs
2152            .try_reserve_exact(refs.len())
2153            .map_err(|_| limit("dependentRequired pair arena"))?;
2154        self.dependent_required_pairs.extend_from_slice(&refs);
2155        let slice = DependentRequiredSlice { off, len };
2156        self.dependent_required_intern.insert(refs, slice);
2157        Ok(slice)
2158    }
2159
2160    fn intern_bits(&mut self, words: Vec<u64>, field_count: u32) -> Result<BitSlice, CompileError> {
2161        if let Some(r) = self.bit_intern.get(&words) {
2162            return Ok(BitSlice {
2163                off: r.off,
2164                len: field_count,
2165            });
2166        }
2167        if self
2168            .required_bits
2169            .len()
2170            .checked_add(words.len())
2171            .is_none_or(|n| n > MAX_REQUIRED_WORDS)
2172        {
2173            return Err(limit("required words cap"));
2174        }
2175        let off = u32::try_from(self.required_bits.len()).map_err(|_| limit("required blob"))?;
2176        self.required_bits.extend_from_slice(&words);
2177        let r = BitSlice {
2178            off,
2179            len: field_count,
2180        };
2181        self.bit_intern.insert(words, r);
2182        Ok(r)
2183    }
2184
2185    fn intern_refs(&mut self, refs: Vec<NodeId>) -> Result<RefSlice, CompileError> {
2186        if let Some(r) = self.ref_intern.get(&refs) {
2187            return Ok(*r);
2188        }
2189        if self
2190            .node_refs
2191            .len()
2192            .checked_add(refs.len())
2193            .is_none_or(|n| n > MAX_NODE_REFS)
2194        {
2195            return Err(limit("node refs cap"));
2196        }
2197        let off = u32::try_from(self.node_refs.len()).map_err(|_| limit("node refs blob"))?;
2198        let len = u32::try_from(refs.len()).map_err(|_| limit("node ref run"))?;
2199        self.node_refs.extend_from_slice(&refs);
2200        let r = RefSlice { off, len };
2201        self.ref_intern.insert(refs, r);
2202        Ok(r)
2203    }
2204
2205    fn intern_node(&mut self, node: Node) -> Result<NodeId, CompileError> {
2206        if let Some(id) = self.node_intern.get(&(self.current_resource, node.clone())) {
2207            return Ok(*id);
2208        }
2209        if self.nodes.len() >= MAX_NODES {
2210            return Err(limit("node cap"));
2211        }
2212        let id = NodeId::try_from(self.nodes.len()).map_err(|_| limit("node count"))?;
2213        self.nodes.push(node.clone());
2214        self.node_resources.push(self.current_resource);
2215        self.node_intern.insert((self.current_resource, node), id);
2216        Ok(id)
2217    }
2218
2219    pub(crate) fn set_current_resource(&mut self, resource: ResourceId) -> ResourceId {
2220        std::mem::replace(&mut self.current_resource, resource)
2221    }
2222
2223    /// Interns a `null` node.
2224    pub fn null(&mut self) -> Result<NodeId, CompileError> {
2225        self.intern_node(Node::Null)
2226    }
2227
2228    /// Interns a `boolean` node.
2229    pub fn boolean(&mut self) -> Result<NodeId, CompileError> {
2230        self.intern_node(Node::Boolean)
2231    }
2232
2233    /// Interns a node matching no value at all (the bare `false` schema).
2234    pub fn never(&mut self) -> Result<NodeId, CompileError> {
2235        self.intern_node(Node::Never)
2236    }
2237
2238    /// Interns a string-const node.
2239    pub fn string_const(&mut self, value: &str) -> Result<NodeId, CompileError> {
2240        let value = self.intern_str(value)?;
2241        self.intern_node(Node::StringConst { value })
2242    }
2243
2244    /// Interns a string-pattern node.
2245    pub fn string_pattern(
2246        &mut self,
2247        regex: &str,
2248        min_len: Option<u32>,
2249        max_len: Option<u32>,
2250        charset: Charset,
2251    ) -> Result<NodeId, CompileError> {
2252        let regex = self.intern_str(regex)?;
2253        self.intern_node(Node::StringPattern {
2254            regex,
2255            min_len,
2256            max_len,
2257            charset,
2258        })
2259    }
2260
2261    /// Interns an integer node.
2262    pub fn integer(
2263        &mut self,
2264        minimum: Option<i64>,
2265        maximum: Option<i64>,
2266    ) -> Result<NodeId, CompileError> {
2267        self.integer_multiple_of(minimum, maximum, None)
2268    }
2269
2270    /// Interns an integer node with an exact `multipleOf` divisor.
2271    pub fn integer_multiple_of(
2272        &mut self,
2273        minimum: Option<i64>,
2274        maximum: Option<i64>,
2275        multiple_of: Option<u64>,
2276    ) -> Result<NodeId, CompileError> {
2277        if multiple_of == Some(0) {
2278            return Err(malformed("multipleOf must be a positive integer"));
2279        }
2280        self.intern_node(Node::Integer {
2281            minimum,
2282            maximum,
2283            multiple_of,
2284        })
2285    }
2286
2287    /// Interns an unbounded `number` node (the full JSON numeric grammar).
2288    pub fn number(&mut self) -> Result<NodeId, CompileError> {
2289        self.number_range(None, None, None)
2290    }
2291
2292    /// Interns a `number` node with an integer-valued inclusive/exclusive bound and/or a decimal
2293    /// `multipleOf` (`coef * 10^-exp`).
2294    pub fn number_range(
2295        &mut self,
2296        minimum: Option<(i128, u32, bool)>,
2297        maximum: Option<(i128, u32, bool)>,
2298        multiple_of: Option<(u64, u32)>,
2299    ) -> Result<NodeId, CompileError> {
2300        self.intern_node(Node::Number {
2301            integer_only: false,
2302            minimum,
2303            maximum,
2304            multiple_of,
2305        })
2306    }
2307
2308    /// Interns exact decimal constraints whose accepted values must be mathematically integral.
2309    pub fn integer_number_range(
2310        &mut self,
2311        minimum: Option<(i128, u32, bool)>,
2312        maximum: Option<(i128, u32, bool)>,
2313        multiple_of: Option<(u64, u32)>,
2314    ) -> Result<NodeId, CompileError> {
2315        self.intern_node(Node::Number {
2316            integer_only: true,
2317            minimum,
2318            maximum,
2319            multiple_of,
2320        })
2321    }
2322
2323    /// Interns a lexical JSON-number constraint.
2324    pub fn lexical_number(&mut self, regex: &str) -> Result<NodeId, CompileError> {
2325        validate_pattern(regex)?;
2326        let regex = self.intern_str(regex)?;
2327        self.intern_node(Node::LexicalNumber { regex })
2328    }
2329
2330    /// Interns an enum node. Values are canonically sorted; a duplicate value is rejected.
2331    pub fn enum_values(&mut self, mut values: Vec<ScalarLit>) -> Result<NodeId, CompileError> {
2332        if values.is_empty() {
2333            return Err(malformed("an enum must have at least one value"));
2334        }
2335        values.sort();
2336        if values.windows(2).any(|w| w[0] == w[1]) {
2337            return Err(malformed("duplicate enum value"));
2338        }
2339        let values = self.intern_lits(values)?;
2340        self.intern_node(Node::Enum { values })
2341    }
2342
2343    /// Interns an array node.
2344    pub fn array(
2345        &mut self,
2346        items: NodeId,
2347        min_items: u32,
2348        max_items: Option<u32>,
2349    ) -> Result<NodeId, CompileError> {
2350        self.array_unique(items, min_items, max_items, false)
2351    }
2352
2353    /// Interns an array node with an explicit `uniqueItems` flag.
2354    pub fn array_unique(
2355        &mut self,
2356        items: NodeId,
2357        min_items: u32,
2358        max_items: Option<u32>,
2359        unique_items: bool,
2360    ) -> Result<NodeId, CompileError> {
2361        self.array_full(
2362            ItemsPolicy::Schema(items),
2363            min_items,
2364            max_items,
2365            unique_items,
2366            None,
2367            true,
2368        )
2369    }
2370
2371    /// Interns an array node with unconstrained (`items: true` or absent) elements.
2372    pub fn array_open(
2373        &mut self,
2374        min_items: u32,
2375        max_items: Option<u32>,
2376        unique_items: bool,
2377    ) -> Result<NodeId, CompileError> {
2378        self.array_full(
2379            ItemsPolicy::AllowAny,
2380            min_items,
2381            max_items,
2382            unique_items,
2383            None,
2384            true,
2385        )
2386    }
2387
2388    /// Interns an array node with every keyword explicit, including `contains` and whether `items`
2389    /// was textually present (`items_annotates`, for `unevaluatedItems`).
2390    #[allow(clippy::too_many_arguments)]
2391    pub fn array_full(
2392        &mut self,
2393        items: ItemsPolicy,
2394        min_items: u32,
2395        max_items: Option<u32>,
2396        unique_items: bool,
2397        contains: Option<ContainsConstraint>,
2398        items_annotates: bool,
2399    ) -> Result<NodeId, CompileError> {
2400        self.intern_node(Node::Array {
2401            items,
2402            min_items,
2403            max_items,
2404            unique_items,
2405            contains,
2406            items_annotates,
2407        })
2408    }
2409
2410    /// Interns a tuple array with positional `prefix` schemas and an optional `tail` schema.
2411    /// Closed tuples whose minimum exceeds the prefix length are rejected.
2412    pub fn tuple(
2413        &mut self,
2414        prefix: Vec<NodeId>,
2415        tail: Option<NodeId>,
2416        min_items: u32,
2417        max_items: Option<u32>,
2418    ) -> Result<NodeId, CompileError> {
2419        self.tuple_unique(prefix, tail, min_items, max_items, false)
2420    }
2421
2422    /// Interns a positional (tuple) array node with an explicit `uniqueItems` flag.
2423    pub fn tuple_unique(
2424        &mut self,
2425        prefix: Vec<NodeId>,
2426        tail: Option<NodeId>,
2427        min_items: u32,
2428        max_items: Option<u32>,
2429        unique_items: bool,
2430    ) -> Result<NodeId, CompileError> {
2431        self.tuple_full(prefix, tail, min_items, max_items, unique_items, None, true)
2432    }
2433
2434    /// Interns a positional (tuple) array node with every keyword explicit, including `contains` and
2435    /// whether the tail was textually present (`tail_annotates`, for `unevaluatedItems`).
2436    #[allow(clippy::too_many_arguments)]
2437    pub fn tuple_full(
2438        &mut self,
2439        prefix: Vec<NodeId>,
2440        tail: Option<NodeId>,
2441        min_items: u32,
2442        max_items: Option<u32>,
2443        unique_items: bool,
2444        contains: Option<ContainsConstraint>,
2445        tail_annotates: bool,
2446    ) -> Result<NodeId, CompileError> {
2447        if prefix.is_empty() {
2448            return Err(malformed("a tuple must have at least one prefix position"));
2449        }
2450        let prefix = self.intern_refs(prefix)?;
2451        self.intern_node(Node::Tuple {
2452            prefix,
2453            tail,
2454            min_items,
2455            max_items,
2456            unique_items,
2457            tail_annotates,
2458            contains,
2459        })
2460    }
2461
2462    /// Interns a type-union node. `branches` are child nodes in declaration order; a single branch
2463    /// collapses to that branch directly rather than a one-element union.
2464    pub fn union_of(&mut self, branches: Vec<NodeId>) -> Result<NodeId, CompileError> {
2465        if branches.is_empty() {
2466            return Err(malformed("a type union must have at least one branch"));
2467        }
2468        if branches.len() == 1 {
2469            return Ok(branches[0]);
2470        }
2471        let branches = self.intern_refs(branches)?;
2472        self.intern_node(Node::Union { branches })
2473    }
2474
2475    /// Reserves a def slot for a recursive definition and returns its index. The slot is filled by
2476    /// [`Builder::set_def_target`] once the body is lowered; a `Node::Ref` may point at it meanwhile.
2477    pub fn alloc_def_slot(&mut self) -> Result<u32, CompileError> {
2478        let slot = u32::try_from(self.def_targets.len()).map_err(|_| limit("def slot count"))?;
2479        if self.def_targets.len() >= MAX_NODES {
2480            return Err(limit("def slot cap"));
2481        }
2482        self.def_targets.push(None);
2483        Ok(slot)
2484    }
2485
2486    /// Points a reserved def slot at its lowered body root. Fails on an out-of-range slot or one
2487    /// already assigned, rather than silently overwriting or indexing out of bounds.
2488    pub fn set_def_target(&mut self, slot: u32, body: NodeId) -> Result<(), CompileError> {
2489        let entry = self
2490            .def_targets
2491            .get_mut(slot as usize)
2492            .ok_or_else(|| malformed("def slot index out of range"))?;
2493        if entry.replace(body).is_some() {
2494            return Err(malformed("def slot already assigned"));
2495        }
2496        Ok(())
2497    }
2498
2499    /// Interns a `Node::Ref` naming a reserved def slot (a recursion cut point).
2500    pub fn ref_node(&mut self, def: u32) -> Result<NodeId, CompileError> {
2501        self.intern_node(Node::Ref { def })
2502    }
2503
2504    /// Interns an already-statically-resolved dynamic reference.
2505    pub fn dynamic_ref(
2506        &mut self,
2507        initial_target: NodeId,
2508        anchor: AnchorId,
2509    ) -> Result<NodeId, CompileError> {
2510        self.intern_node(Node::DynamicRef {
2511            initial_target,
2512            anchor,
2513        })
2514    }
2515
2516    /// Interns an `unevaluatedProperties`/`unevaluatedItems` obligation over `scope`, with
2517    /// `unevaluated` applied to each member `scope` did not annotate.
2518    pub fn unevaluated(
2519        &mut self,
2520        kind: UnevaluatedKind,
2521        scope: NodeId,
2522        unevaluated: NodeId,
2523    ) -> Result<NodeId, CompileError> {
2524        self.intern_node(Node::Unevaluated {
2525            kind,
2526            scope,
2527            unevaluated,
2528        })
2529    }
2530
2531    /// Interns an `allOf` intersection in declaration order.
2532    /// A single branch collapses to itself.
2533    pub fn intersection_of(&mut self, branches: Vec<NodeId>) -> Result<NodeId, CompileError> {
2534        if branches.is_empty() {
2535            return Err(malformed("an intersection must have at least one branch"));
2536        }
2537        if branches.len() == 1 {
2538            return Ok(branches[0]);
2539        }
2540        let branches = self.intern_refs(branches)?;
2541        self.intern_node(Node::Intersection { branches })
2542    }
2543
2544    /// Interns a `oneOf` exactly-one node in declaration order.
2545    /// A single branch collapses to itself because exactly-one preserves its language.
2546    pub fn exactly_one_of(&mut self, branches: Vec<NodeId>) -> Result<NodeId, CompileError> {
2547        if branches.is_empty() {
2548            return Err(malformed("an exactly-one must have at least one branch"));
2549        }
2550        if branches.len() == 1 {
2551            return Ok(branches[0]);
2552        }
2553        let branches = self.intern_refs(branches)?;
2554        self.intern_node(Node::ExactlyOne { branches })
2555    }
2556
2557    /// Interns a `not`: matches any complete JSON value `inner` does not.
2558    pub fn not(&mut self, inner: NodeId) -> Result<NodeId, CompileError> {
2559        self.intern_node(Node::Not { inner })
2560    }
2561
2562    /// Interns `(name, child)` pairs plus their aligned required bitset; shared by `object` and
2563    /// `open_object`. A duplicate field name is rejected.
2564    fn intern_named_fields(
2565        &mut self,
2566        fields: Vec<(String, NodeId)>,
2567        required: &[bool],
2568    ) -> Result<(PropSlice, BitSlice), CompileError> {
2569        if required.len() != fields.len() {
2570            return Err(malformed("required flags do not align with fields"));
2571        }
2572        let mut names: FxHashMap<&str, ()> = FxHashMap::default();
2573        names.reserve(fields.len());
2574        for (name, _) in &fields {
2575            if names.insert(name.as_str(), ()).is_some() {
2576                return Err(malformed("duplicate object key"));
2577            }
2578        }
2579        let mut pairs = Vec::with_capacity(fields.len());
2580        for (name, child) in &fields {
2581            let name = self.intern_str(name)?;
2582            pairs.push((name, *child));
2583        }
2584        let field_count = u32::try_from(fields.len()).map_err(|_| limit("object field count"))?;
2585        let words = fields.len().div_ceil(64);
2586        let mut bits = vec![0u64; words];
2587        for (i, &req) in required.iter().enumerate() {
2588            if req {
2589                bits[i / 64] |= 1u64 << (i % 64);
2590            }
2591        }
2592        let fields = self.intern_props(pairs)?;
2593        let required = self.intern_bits(bits, field_count)?;
2594        Ok((fields, required))
2595    }
2596
2597    /// Interns an object node. `fields` are `(name, child)` in source order; `required` is aligned
2598    /// to `fields` by position. A duplicate field name is rejected.
2599    pub fn object(
2600        &mut self,
2601        fields: Vec<(String, NodeId)>,
2602        required: &[bool],
2603        closure: ObjectClosure,
2604    ) -> Result<NodeId, CompileError> {
2605        self.object_full(fields, required, closure, Vec::new(), Vec::new())
2606    }
2607
2608    /// Interns an object with `dependentSchemas`: `(key, schema)` pairs where `key`'s presence in
2609    /// the instance additionally requires the whole instance to satisfy `schema`.
2610    pub fn object_full(
2611        &mut self,
2612        fields: Vec<(String, NodeId)>,
2613        required: &[bool],
2614        closure: ObjectClosure,
2615        dependent: Vec<(String, NodeId)>,
2616        dependent_required: Vec<(String, Vec<String>)>,
2617    ) -> Result<NodeId, CompileError> {
2618        let (fields, required) = self.intern_named_fields(fields, required)?;
2619        let dependent = self.intern_dependent(dependent)?;
2620        let dependent_required = self.intern_dependent_required(dependent_required)?;
2621        self.intern_node(Node::Object {
2622            fields,
2623            required,
2624            closure,
2625            dependent,
2626            dependent_required,
2627        })
2628    }
2629
2630    fn intern_dependent(
2631        &mut self,
2632        dependent: Vec<(String, NodeId)>,
2633    ) -> Result<PropSlice, CompileError> {
2634        let mut pairs = Vec::with_capacity(dependent.len());
2635        for (name, child) in dependent {
2636            let name_ref = self.intern_str(&name)?;
2637            pairs.push((name_ref, child));
2638        }
2639        self.intern_props(pairs)
2640    }
2641
2642    /// Interns an order-independent open object. `known`/`known_required` behave like `object`'s
2643    /// `fields`/`required`; `patterns` are `(regex, child)` `patternProperties` entries.
2644    #[allow(clippy::too_many_arguments)]
2645    pub fn open_object(
2646        &mut self,
2647        known: Vec<(String, NodeId)>,
2648        known_required: &[bool],
2649        patterns: Vec<(String, NodeId)>,
2650        additional: AdditionalPolicy,
2651        property_names: Option<NodeId>,
2652        min_properties: Option<u32>,
2653        max_properties: Option<u32>,
2654    ) -> Result<NodeId, CompileError> {
2655        self.open_object_full(
2656            known,
2657            known_required,
2658            patterns,
2659            additional,
2660            property_names,
2661            min_properties,
2662            max_properties,
2663            Vec::new(),
2664            Vec::new(),
2665        )
2666    }
2667
2668    /// Interns an order-independent open object with `dependentSchemas`, same semantics as
2669    /// `object_full`'s.
2670    #[allow(clippy::too_many_arguments)]
2671    pub fn open_object_full(
2672        &mut self,
2673        known: Vec<(String, NodeId)>,
2674        known_required: &[bool],
2675        patterns: Vec<(String, NodeId)>,
2676        additional: AdditionalPolicy,
2677        property_names: Option<NodeId>,
2678        min_properties: Option<u32>,
2679        max_properties: Option<u32>,
2680        dependent: Vec<(String, NodeId)>,
2681        dependent_required: Vec<(String, Vec<String>)>,
2682    ) -> Result<NodeId, CompileError> {
2683        if let (Some(lo), Some(hi)) = (min_properties, max_properties) {
2684            if hi < lo {
2685                return Err(malformed("maxProperties is less than minProperties"));
2686            }
2687        }
2688        let (known, known_required) = self.intern_named_fields(known, known_required)?;
2689        let mut pattern_pairs = Vec::with_capacity(patterns.len());
2690        for (regex, child) in &patterns {
2691            let regex_ref = self.intern_str(regex)?;
2692            pattern_pairs.push((regex_ref, *child));
2693        }
2694        let patterns = self.intern_props(pattern_pairs)?;
2695        let dependent = self.intern_dependent(dependent)?;
2696        let dependent_required = self.intern_dependent_required(dependent_required)?;
2697        self.intern_node(Node::OpenObject {
2698            known,
2699            known_required,
2700            patterns,
2701            additional,
2702            property_names,
2703            min_properties,
2704            max_properties,
2705            dependent,
2706            dependent_required,
2707        })
2708    }
2709
2710    /// Interns an `Unsupported` node and records a diagnostic for this occurrence. Returns the
2711    /// node id, or `TooManyDiagnostics` once the bounded buffer is full.
2712    pub fn unsupported(
2713        &mut self,
2714        keyword: &str,
2715        reason: UnsupportedReason,
2716        json_pointer: &str,
2717    ) -> Result<NodeId, CompileError> {
2718        if self.diagnostics.len() >= MAX_DIAGNOSTICS
2719            || self.diagnostics.len() as u64 >= u64::from(self.options.max_diagnostics)
2720        {
2721            return Err(CompileError::new(
2722                ErrorCode::TooManyDiagnostics,
2723                Stage::L2,
2724                "diagnostics buffer is full",
2725            ));
2726        }
2727        let keyword_ref = self.intern_str(keyword)?;
2728        let pointer_ref = self.intern_str(json_pointer)?;
2729        let node = self.intern_node(Node::Unsupported {
2730            keyword: keyword_ref,
2731            reason,
2732        })?;
2733        self.diagnostics.push(Diagnostic {
2734            node,
2735            keyword: keyword_ref,
2736            json_pointer: pointer_ref,
2737            source_span: None,
2738            reason,
2739        });
2740        Ok(node)
2741    }
2742
2743    /// Freezes the arena into an immutable IR rooted at `root`, running full validation.
2744    pub fn finish(self, root: NodeId) -> Result<SchemaIR, CompileError> {
2745        self.finish_with_resources(
2746            root,
2747            None,
2748            vec![ResourceInput {
2749                canonical_uri: None,
2750                root,
2751                static_anchors: Vec::new(),
2752                dynamic_anchors: Vec::new(),
2753            }],
2754        )
2755    }
2756
2757    pub(crate) fn finish_with_resources(
2758        mut self,
2759        root: NodeId,
2760        retrieval_uri: Option<String>,
2761        resource_inputs: Vec<ResourceInput>,
2762    ) -> Result<SchemaIR, CompileError> {
2763        let retrieval_uri = retrieval_uri
2764            .as_deref()
2765            .map(|uri| self.intern_str(uri))
2766            .transpose()?;
2767        let mut resources = Vec::new();
2768        resources
2769            .try_reserve_exact(resource_inputs.len())
2770            .map_err(|_| limit("resource allocation"))?;
2771        let mut anchors = Vec::new();
2772        for (index, input) in resource_inputs.into_iter().enumerate() {
2773            let resource = ResourceId(u32::try_from(index).map_err(|_| limit("resource index"))?);
2774            let canonical_uri = input
2775                .canonical_uri
2776                .as_deref()
2777                .map(|uri| self.intern_str(uri))
2778                .transpose()?;
2779            let static_off = u32::try_from(anchors.len()).map_err(|_| limit("anchor offset"))?;
2780            for (name, target) in input.static_anchors {
2781                let name = self.intern_str(&name)?;
2782                anchors.push(AnchorRecord {
2783                    resource,
2784                    name,
2785                    target,
2786                });
2787            }
2788            let static_len = u32::try_from(anchors.len())
2789                .map_err(|_| limit("anchor count"))?
2790                .checked_sub(static_off)
2791                .ok_or_else(|| limit("anchor run"))?;
2792            let dynamic_off = u32::try_from(anchors.len()).map_err(|_| limit("anchor offset"))?;
2793            for (name, target) in input.dynamic_anchors {
2794                let name = self.intern_str(&name)?;
2795                anchors.push(AnchorRecord {
2796                    resource,
2797                    name,
2798                    target,
2799                });
2800            }
2801            let dynamic_len = u32::try_from(anchors.len())
2802                .map_err(|_| limit("anchor count"))?
2803                .checked_sub(dynamic_off)
2804                .ok_or_else(|| limit("anchor run"))?;
2805            resources.push(ResourceRecord {
2806                canonical_uri,
2807                root: input.root,
2808                static_anchors: AnchorSlice {
2809                    off: static_off,
2810                    len: static_len,
2811                },
2812                dynamic_anchors: AnchorSlice {
2813                    off: dynamic_off,
2814                    len: dynamic_len,
2815                },
2816            });
2817        }
2818        let def_targets = self
2819            .def_targets
2820            .into_iter()
2821            .map(|t| t.ok_or_else(|| malformed("def slot never assigned a target")))
2822            .collect::<Result<Vec<NodeId>, CompileError>>()?;
2823        SchemaIR::assemble(SchemaIRWire {
2824            ir_version: IR_VERSION,
2825            root,
2826            nodes: self.nodes,
2827            strings: self.strings,
2828            literals: self.literals,
2829            props: self.props,
2830            dependent_required_pairs: self.dependent_required_pairs,
2831            required_bits: self.required_bits,
2832            node_refs: self.node_refs,
2833            def_targets,
2834            retrieval_uri,
2835            resources,
2836            anchors,
2837            node_resources: self.node_resources,
2838            options: self.options,
2839            diagnostics: self.diagnostics,
2840        })
2841    }
2842}
2843
2844fn limit(what: &'static str) -> CompileError {
2845    CompileError::new(ErrorCode::InternalLimitExceeded, Stage::L2, what)
2846}
2847
2848#[cfg(test)]
2849mod tests {
2850    use super::*;
2851
2852    fn opts() -> CompileOptions {
2853        CompileOptions::default()
2854    }
2855
2856    #[test]
2857    fn identical_subschemas_share_one_node_id() {
2858        let mut b = Builder::new(opts());
2859        let a = b.boolean().unwrap();
2860        let c = b.boolean().unwrap();
2861        assert_eq!(a, c, "boolean is interned once");
2862        let obj = b
2863            .object(
2864                vec![("x".into(), a), ("y".into(), c)],
2865                &[false, false],
2866                ObjectClosure::Forbidden,
2867            )
2868            .unwrap();
2869        let ir = b.finish(obj).unwrap();
2870        // two occurrences of boolean, one distinct boolean node (plus the object).
2871        assert_eq!(ir.node_count(), 2);
2872    }
2873
2874    #[test]
2875    fn finish_rejects_an_unfilled_def_slot() {
2876        let mut b = Builder::new(opts());
2877        let slot = b.alloc_def_slot().unwrap();
2878        let root = b.ref_node(slot).unwrap();
2879        assert!(b.finish(root).is_err());
2880    }
2881
2882    #[test]
2883    fn set_def_target_rejects_a_second_assignment_to_the_same_slot() {
2884        let mut b = Builder::new(opts());
2885        let slot = b.alloc_def_slot().unwrap();
2886        let a = b.boolean().unwrap();
2887        let c = b.boolean().unwrap();
2888        b.set_def_target(slot, a).unwrap();
2889        assert!(b.set_def_target(slot, c).is_err());
2890    }
2891
2892    #[test]
2893    fn set_def_target_rejects_an_out_of_range_slot() {
2894        let mut b = Builder::new(opts());
2895        let a = b.boolean().unwrap();
2896        assert!(b.set_def_target(0, a).is_err());
2897    }
2898
2899    #[test]
2900    fn object_with_few_fields_but_heavy_bounded_strings_needs_structured_backend() {
2901        let mut b = Builder::new(opts());
2902        let a = b
2903            .string_pattern("(?:.)", None, Some(100), Charset::Utf8CountedCodepoints)
2904            .unwrap();
2905        let c = b
2906            .string_pattern("(?:.)", None, Some(2048), Charset::Utf8CountedCodepoints)
2907            .unwrap();
2908        let obj = b
2909            .object(
2910                vec![("a".into(), a), ("c".into(), c)],
2911                &[false, false],
2912                ObjectClosure::Forbidden,
2913            )
2914            .unwrap();
2915        let ir = b.finish(obj).unwrap();
2916        assert!(
2917            ir.requires_structured_backend(),
2918            "2 fields, well under MAX_SAFE_SHUFFLE_FIELDS, but weight 100+2048 exceeds the cap"
2919        );
2920    }
2921
2922    #[test]
2923    fn three_fields_with_a_255_scalar_string_exceed_the_shuffle_edge_bound() {
2924        let mut b = Builder::new(opts());
2925        let label = b
2926            .string_pattern("(?:.)", Some(2), Some(255), Charset::Utf8CountedCodepoints)
2927            .unwrap();
2928        let boolean = b.boolean().unwrap();
2929        let obj = b
2930            .object(
2931                vec![
2932                    ("category".into(), label),
2933                    ("generate_entities".into(), boolean),
2934                    ("label".into(), label),
2935                ],
2936                &[false, false, true],
2937                ObjectClosure::Forbidden,
2938            )
2939            .unwrap();
2940        let ir = b.finish(obj).unwrap();
2941        assert!(ir.requires_structured_backend());
2942    }
2943
2944    #[test]
2945    fn large_minimum_array_needs_structured_backend_even_without_a_maximum() {
2946        let mut b = Builder::new(opts());
2947        let item = b.null().unwrap();
2948        let array = b.array(item, MAX_UNROLLED_ARRAY_ITEMS + 1, None).unwrap();
2949        let ir = b.finish(array).unwrap();
2950        assert!(ir.requires_structured_backend());
2951    }
2952
2953    #[test]
2954    fn object_with_small_bounded_strings_stays_on_the_fast_dfa_path() {
2955        let mut b = Builder::new(opts());
2956        let a = b
2957            .string_pattern("(?:.)", None, Some(50), Charset::Utf8CountedCodepoints)
2958            .unwrap();
2959        let c = b
2960            .string_pattern("(?:.)", None, Some(50), Charset::Utf8CountedCodepoints)
2961            .unwrap();
2962        let obj = b
2963            .object(
2964                vec![("a".into(), a), ("c".into(), c)],
2965                &[false, false],
2966                ObjectClosure::Forbidden,
2967            )
2968            .unwrap();
2969        let ir = b.finish(obj).unwrap();
2970        assert!(!ir.requires_structured_backend());
2971    }
2972
2973    #[test]
2974    fn single_optional_heavy_bounded_string_routes_before_shuffle_construction() {
2975        let mut b = Builder::new(opts());
2976        let a = b
2977            .string_pattern("(?:.)", None, Some(4096), Charset::Utf8CountedCodepoints)
2978            .unwrap();
2979        let obj = b
2980            .object(vec![("a".into(), a)], &[false], ObjectClosure::Forbidden)
2981            .unwrap();
2982        let ir = b.finish(obj).unwrap();
2983        assert!(ir.requires_structured_backend());
2984    }
2985
2986    #[test]
2987    fn heavy_bounded_string_wrapped_in_a_nullable_union_still_counts_its_weight() {
2988        let mut b = Builder::new(opts());
2989        let null = b.null().unwrap();
2990        let big = b
2991            .string_pattern("(?:.)", None, Some(2048), Charset::Utf8CountedCodepoints)
2992            .unwrap();
2993        let nullable_big = b.union_of(vec![null, big]).unwrap();
2994        let small = b
2995            .string_pattern("(?:.)", None, Some(100), Charset::Utf8CountedCodepoints)
2996            .unwrap();
2997        let obj = b
2998            .object(
2999                vec![("a".into(), nullable_big), ("b".into(), small)],
3000                &[false, false],
3001                ObjectClosure::Forbidden,
3002            )
3003            .unwrap();
3004        let ir = b.finish(obj).unwrap();
3005        assert!(
3006            ir.requires_structured_backend(),
3007            "type:[X,null] must not hide a field's real weight from the shuffle pre-check"
3008        );
3009    }
3010
3011    #[test]
3012    fn distinct_node_count_never_exceeds_occurrence_count() {
3013        let mut b = Builder::new(opts());
3014        let inner = b.boolean().unwrap();
3015        let arr = b.array(inner, 0, None).unwrap();
3016        let ir = b.finish(arr).unwrap();
3017        assert!(ir.node_count() <= 2);
3018    }
3019
3020    #[test]
3021    fn two_builders_different_insertion_order_hash_equal() {
3022        let mut b1 = Builder::new(opts());
3023        let x = b1.boolean().unwrap();
3024        let y = b1.null().unwrap();
3025        let o1 = b1
3026            .object(
3027                vec![("a".into(), x), ("b".into(), y)],
3028                &[true, false],
3029                ObjectClosure::Forbidden,
3030            )
3031            .unwrap();
3032        let ir1 = b1.finish(o1).unwrap();
3033
3034        let mut b2 = Builder::new(opts());
3035        let y = b2.null().unwrap();
3036        let x = b2.boolean().unwrap();
3037        let o2 = b2
3038            .object(
3039                vec![("a".into(), x), ("b".into(), y)],
3040                &[true, false],
3041                ObjectClosure::Forbidden,
3042            )
3043            .unwrap();
3044        let ir2 = b2.finish(o2).unwrap();
3045
3046        assert_eq!(ir1.canonical_hash(), ir2.canonical_hash());
3047    }
3048
3049    #[test]
3050    fn different_property_order_changes_the_hash() {
3051        let mut b1 = Builder::new(opts());
3052        let (x, y) = (b1.boolean().unwrap(), b1.null().unwrap());
3053        let o1 = b1
3054            .object(
3055                vec![("a".into(), x), ("b".into(), y)],
3056                &[false, false],
3057                ObjectClosure::Forbidden,
3058            )
3059            .unwrap();
3060        let ir1 = b1.finish(o1).unwrap();
3061
3062        let mut b2 = Builder::new(opts());
3063        let (x, y) = (b2.boolean().unwrap(), b2.null().unwrap());
3064        let o2 = b2
3065            .object(
3066                vec![("b".into(), y), ("a".into(), x)],
3067                &[false, false],
3068                ObjectClosure::Forbidden,
3069            )
3070            .unwrap();
3071        let ir2 = b2.finish(o2).unwrap();
3072
3073        assert_ne!(ir1.canonical_hash(), ir2.canonical_hash());
3074    }
3075
3076    #[test]
3077    fn enum_order_does_not_change_the_hash() {
3078        let mut b1 = Builder::new(opts());
3079        let e1 = b1
3080            .enum_values(vec![ScalarLit::Int(1), ScalarLit::Int(2)])
3081            .unwrap();
3082        let ir1 = b1.finish(e1).unwrap();
3083        let mut b2 = Builder::new(opts());
3084        let e2 = b2
3085            .enum_values(vec![ScalarLit::Int(2), ScalarLit::Int(1)])
3086            .unwrap();
3087        let ir2 = b2.finish(e2).unwrap();
3088        assert_eq!(ir1.canonical_hash(), ir2.canonical_hash());
3089    }
3090
3091    #[test]
3092    fn required_flag_change_changes_the_hash() {
3093        let mk = |req: &[bool]| {
3094            let mut b = Builder::new(opts());
3095            let x = b.boolean().unwrap();
3096            let o = b
3097                .object(vec![("a".into(), x)], req, ObjectClosure::Forbidden)
3098                .unwrap();
3099            b.finish(o).unwrap().canonical_hash()
3100        };
3101        assert_ne!(mk(&[true]), mk(&[false]));
3102    }
3103
3104    #[test]
3105    fn length_prefix_prevents_concatenation_collision() {
3106        let mut b1 = Builder::new(opts());
3107        let e1 = b1
3108            .enum_values(vec![
3109                ScalarLit::Str("ab".into()),
3110                ScalarLit::Str("c".into()),
3111            ])
3112            .unwrap();
3113        let ir1 = b1.finish(e1).unwrap();
3114        let mut b2 = Builder::new(opts());
3115        let e2 = b2
3116            .enum_values(vec![
3117                ScalarLit::Str("a".into()),
3118                ScalarLit::Str("bc".into()),
3119            ])
3120            .unwrap();
3121        let ir2 = b2.finish(e2).unwrap();
3122        assert_ne!(ir1.canonical_hash(), ir2.canonical_hash());
3123    }
3124
3125    #[test]
3126    fn enum_rejects_duplicate_value() {
3127        let mut b = Builder::new(opts());
3128        let e = b.enum_values(vec![ScalarLit::Int(1), ScalarLit::Int(1)]);
3129        assert_eq!(e.unwrap_err().code, ErrorCode::Malformed);
3130    }
3131
3132    #[test]
3133    fn object_rejects_duplicate_key() {
3134        let mut b = Builder::new(opts());
3135        let x = b.boolean().unwrap();
3136        let y = b.null().unwrap();
3137        let e = b.object(
3138            vec![("a".into(), x), ("a".into(), y)],
3139            &[false, false],
3140            ObjectClosure::Forbidden,
3141        );
3142        assert_eq!(e.unwrap_err().code, ErrorCode::Malformed);
3143    }
3144
3145    #[test]
3146    fn array_max_below_min_is_rejected_at_finish() {
3147        let mut b = Builder::new(opts());
3148        let inner = b.boolean().unwrap();
3149        let arr = b.array(inner, 3, Some(2)).unwrap();
3150        assert_eq!(b.finish(arr).unwrap_err().code, ErrorCode::Malformed);
3151    }
3152
3153    #[test]
3154    fn unsupported_records_a_diagnostic_per_occurrence() {
3155        let mut b = Builder::new(opts());
3156        let u1 = b
3157            .unsupported("anyOf", UnsupportedReason::UnionType, "/anyOf")
3158            .unwrap();
3159        let u2 = b
3160            .unsupported("anyOf", UnsupportedReason::UnionType, "/properties/x/anyOf")
3161            .unwrap();
3162        assert_eq!(u1, u2, "same keyword+reason interns to one node");
3163        let ir = b.finish(u1).unwrap();
3164        assert_eq!(
3165            ir.diagnostics().len(),
3166            2,
3167            "each occurrence keeps its own pointer"
3168        );
3169        let pointers: Vec<&str> = ir
3170            .diagnostics()
3171            .map(|d| ir.str_at(d.json_pointer).unwrap())
3172            .collect();
3173        assert_eq!(pointers, ["/anyOf", "/properties/x/anyOf"]);
3174    }
3175
3176    #[test]
3177    fn diagnostics_are_bounded() {
3178        let mut options = opts();
3179        options.max_diagnostics = 2;
3180        let mut b = Builder::new(options);
3181        b.unsupported("not", UnsupportedReason::Complement, "/a")
3182            .unwrap();
3183        b.unsupported("not", UnsupportedReason::Complement, "/b")
3184            .unwrap();
3185        let e = b.unsupported("not", UnsupportedReason::Complement, "/c");
3186        assert_eq!(e.unwrap_err().code, ErrorCode::TooManyDiagnostics);
3187    }
3188
3189    #[test]
3190    fn default_max_diagnostics_absorbs_a_real_world_scale_schema_without_hard_failing() {
3191        // The measured jsonschemabench worst case was ~4500 occurrences; the default must clear it.
3192        let mut b = Builder::new(CompileOptions::default());
3193        for i in 0..5000u32 {
3194            b.unsupported("not", UnsupportedReason::Complement, &format!("/{i}"))
3195                .unwrap();
3196        }
3197        let last = b
3198            .unsupported("not", UnsupportedReason::Complement, "/last")
3199            .unwrap();
3200        let ir = b.finish(last).unwrap();
3201        assert_eq!(ir.diagnostics().len(), 5001);
3202    }
3203
3204    #[test]
3205    fn canonical_hash_ignores_diagnostic_pointer() {
3206        let mk = |ptr: &str| {
3207            let mut b = Builder::new(opts());
3208            let u = b
3209                .unsupported("not", UnsupportedReason::Complement, ptr)
3210                .unwrap();
3211            b.finish(u).unwrap().canonical_hash()
3212        };
3213        assert_eq!(mk("/a"), mk("/properties/deeply/nested/b"));
3214    }
3215
3216    #[test]
3217    fn integer_bounds_reject_empty_range_at_finish() {
3218        let mut b = Builder::new(opts());
3219        let n = b.integer(Some(5), Some(1)).unwrap();
3220        assert_eq!(b.finish(n).unwrap_err().code, ErrorCode::Malformed);
3221    }
3222
3223    #[test]
3224    fn object_closure_change_changes_the_hash() {
3225        let mk = |closure| {
3226            let mut b = Builder::new(opts());
3227            let x = b.boolean().unwrap();
3228            let o = b.object(vec![("a".into(), x)], &[false], closure).unwrap();
3229            b.finish(o).unwrap().canonical_hash()
3230        };
3231        assert_ne!(
3232            mk(ObjectClosure::Forbidden),
3233            mk(ObjectClosure::AssumeClosedProfile)
3234        );
3235    }
3236
3237    #[test]
3238    fn compile_options_are_part_of_the_program4_hash() {
3239        let mk = |cap| {
3240            let mut o = opts();
3241            o.max_diagnostics = cap;
3242            let mut b = Builder::new(o);
3243            let n = b.boolean().unwrap();
3244            b.finish(n).unwrap().canonical_hash()
3245        };
3246        assert_ne!(mk(8), mk(64));
3247    }
3248
3249    #[test]
3250    fn all_compile_options_change_the_program4_hash() {
3251        let mk = |f: &dyn Fn(&mut CompileOptions)| {
3252            let mut o = opts();
3253            f(&mut o);
3254            let mut b = Builder::new(o);
3255            let n = b.boolean().unwrap();
3256            b.finish(n).unwrap().canonical_hash()
3257        };
3258        let base = mk(&|_| {});
3259        assert_ne!(base, mk(&|o| o.max_diagnostics = 8));
3260        assert_ne!(base, mk(&|o| o.format_assertion = true));
3261    }
3262
3263    #[test]
3264    fn same_content_address_holds_across_build_order() {
3265        let mut b1 = Builder::new(opts());
3266        let x = b1.boolean().unwrap();
3267        let y = b1.null().unwrap();
3268        let o1 = b1
3269            .object(
3270                vec![("a".into(), x), ("b".into(), y)],
3271                &[true, false],
3272                ObjectClosure::Forbidden,
3273            )
3274            .unwrap();
3275        let ir1 = b1.finish(o1).unwrap();
3276
3277        let mut b2 = Builder::new(opts());
3278        let y = b2.null().unwrap();
3279        let x = b2.boolean().unwrap();
3280        let o2 = b2
3281            .object(
3282                vec![("a".into(), x), ("b".into(), y)],
3283                &[true, false],
3284                ObjectClosure::Forbidden,
3285            )
3286            .unwrap();
3287        let ir2 = b2.finish(o2).unwrap();
3288
3289        assert!(ir1.same_content_address(&ir2));
3290    }
3291
3292    #[test]
3293    fn object_field_counts_around_word_boundaries_build_and_read_back() {
3294        for n in [0usize, 1, 63, 64, 65, 130] {
3295            let mut b = Builder::new(opts());
3296            let mut fields = Vec::with_capacity(n);
3297            let mut required = Vec::with_capacity(n);
3298            for i in 0..n {
3299                let child = b.boolean().unwrap();
3300                fields.push((format!("f{i}"), child));
3301                required.push(i % 2 == 0);
3302            }
3303            let o = b
3304                .object(fields, &required, ObjectClosure::Forbidden)
3305                .unwrap();
3306            let ir = b.finish(o).unwrap();
3307            let Node::Object { required: bits, .. } = ir.node(o).unwrap() else {
3308                panic!("object");
3309            };
3310            for i in 0..n {
3311                let idx = u32::try_from(i).unwrap();
3312                assert_eq!(ir.is_required(*bits, idx), i % 2 == 0, "field {i} of {n}");
3313            }
3314        }
3315    }
3316
3317    #[test]
3318    fn integer_and_enum_carry_i64_extremes() {
3319        let mut b = Builder::new(opts());
3320        let n = b.integer(Some(i64::MIN), Some(i64::MAX)).unwrap();
3321        let ir = b.finish(n).unwrap();
3322        assert_eq!(SchemaIR::from_wire(&ir.to_wire()).unwrap(), ir);
3323
3324        let mut b = Builder::new(opts());
3325        let e = b
3326            .enum_values(vec![ScalarLit::Int(i64::MIN), ScalarLit::Int(i64::MAX)])
3327            .unwrap();
3328        let ir = b.finish(e).unwrap();
3329        assert_eq!(SchemaIR::from_wire(&ir.to_wire()).unwrap(), ir);
3330    }
3331
3332    #[test]
3333    fn literal_string_bytes_at_the_cap_build_and_round_trip() {
3334        let mut b = Builder::new(opts());
3335        let big = "a".repeat(MAX_LITERAL_STR_BYTES);
3336        let e = b.enum_values(vec![ScalarLit::Str(big)]).unwrap();
3337        let ir = b.finish(e).unwrap();
3338        assert_eq!(SchemaIR::from_wire(&ir.to_wire()).unwrap(), ir);
3339    }
3340
3341    #[test]
3342    fn a_single_over_cap_literal_string_is_rejected_at_build() {
3343        let mut b = Builder::new(opts());
3344        let too_big = "a".repeat(MAX_LITERAL_STR_BYTES + 1);
3345        let e = b.enum_values(vec![ScalarLit::Str(too_big)]);
3346        assert_eq!(e.unwrap_err().code, ErrorCode::InternalLimitExceeded);
3347    }
3348
3349    #[test]
3350    fn repeated_large_literals_cannot_bypass_the_aggregate_cap() {
3351        let half = MAX_LITERAL_STR_BYTES / 2;
3352        let mut b = Builder::new(opts());
3353        // Two distinct enums, each just under the cap, together exceed it.
3354        b.enum_values(vec![ScalarLit::Str("a".repeat(half))])
3355            .unwrap();
3356        let second = b.enum_values(vec![ScalarLit::Str("b".repeat(half + 1))]);
3357        assert_eq!(second.unwrap_err().code, ErrorCode::InternalLimitExceeded);
3358    }
3359
3360    #[test]
3361    fn tuple_node_round_trips_and_rejects_a_closed_min_past_the_prefix() {
3362        let mut b = Builder::new(opts());
3363        let x = b.boolean().unwrap();
3364        let y = b.null().unwrap();
3365        let node = b.tuple(vec![x, y], Some(x), 1, Some(4)).unwrap();
3366        let ir = b.finish(node).unwrap();
3367        assert_eq!(SchemaIR::from_wire(&ir.to_wire()).unwrap(), ir);
3368
3369        // A closed tuple (no tail) cannot hold more items than its prefix, so minItems past the
3370        // prefix length is an empty language, rejected at finish.
3371        let mut b = Builder::new(opts());
3372        let x = b.boolean().unwrap();
3373        let node = b.tuple(vec![x], None, 3, None).unwrap();
3374        assert_eq!(b.finish(node).unwrap_err().code, ErrorCode::Malformed);
3375    }
3376
3377    #[test]
3378    fn empty_tuple_prefix_is_rejected_at_build() {
3379        let mut b = Builder::new(opts());
3380        assert_eq!(
3381            b.tuple(vec![], None, 0, None).unwrap_err().code,
3382            ErrorCode::Malformed
3383        );
3384    }
3385
3386    #[test]
3387    fn tuple_and_array_hash_differently_even_with_one_prefix_element() {
3388        let mk_tuple = || {
3389            let mut b = Builder::new(opts());
3390            let x = b.boolean().unwrap();
3391            let n = b.tuple(vec![x], Some(x), 0, None).unwrap();
3392            b.finish(n).unwrap().canonical_hash()
3393        };
3394        let mk_array = || {
3395            let mut b = Builder::new(opts());
3396            let x = b.boolean().unwrap();
3397            let n = b.array(x, 0, None).unwrap();
3398            b.finish(n).unwrap().canonical_hash()
3399        };
3400        assert_ne!(mk_tuple(), mk_array());
3401    }
3402
3403    #[test]
3404    fn deeply_nested_and_repeated_subtrees_build_via_arena_iteration() {
3405        let mut b = Builder::new(opts());
3406        let mut node = b.boolean().unwrap();
3407        for _ in 0..5_000 {
3408            node = b.array(node, 0, None).unwrap();
3409        }
3410        let ir = b.finish(node).unwrap();
3411        assert_eq!(ir.node_count(), 5_001);
3412        let _ = ir.canonical_hash();
3413    }
3414
3415    #[test]
3416    fn open_object_rejects_duplicate_known_keys() {
3417        let mut b = Builder::new(opts());
3418        let x = b.boolean().unwrap();
3419        let err = b
3420            .open_object(
3421                vec![("a".into(), x), ("a".into(), x)],
3422                &[false, false],
3423                Vec::new(),
3424                AdditionalPolicy::Forbid,
3425                None,
3426                None,
3427                None,
3428            )
3429            .unwrap_err();
3430        assert_eq!(err.code, ErrorCode::Malformed);
3431    }
3432
3433    #[test]
3434    fn open_object_rejects_max_properties_below_min_properties() {
3435        let mut b = Builder::new(opts());
3436        let err = b
3437            .open_object(
3438                Vec::new(),
3439                &[],
3440                Vec::new(),
3441                AdditionalPolicy::AllowAny,
3442                None,
3443                Some(5),
3444                Some(2),
3445            )
3446            .unwrap_err();
3447        assert_eq!(err.code, ErrorCode::Malformed);
3448    }
3449
3450    #[test]
3451    fn open_object_additional_policy_changes_the_hash() {
3452        let mk = |policy: AdditionalPolicy| {
3453            let mut b = Builder::new(opts());
3454            let n = b
3455                .open_object(Vec::new(), &[], Vec::new(), policy, None, None, None)
3456                .unwrap();
3457            b.finish(n).unwrap().canonical_hash()
3458        };
3459        let forbid = mk(AdditionalPolicy::Forbid);
3460        let allow_any = mk(AdditionalPolicy::AllowAny);
3461        assert_ne!(forbid, allow_any);
3462
3463        let mut b = Builder::new(opts());
3464        let s = b.boolean().unwrap();
3465        let schema_hash = {
3466            let n = b
3467                .open_object(
3468                    Vec::new(),
3469                    &[],
3470                    Vec::new(),
3471                    AdditionalPolicy::Schema(s),
3472                    None,
3473                    None,
3474                    None,
3475                )
3476                .unwrap();
3477            b.finish(n).unwrap().canonical_hash()
3478        };
3479        assert_ne!(schema_hash, forbid);
3480        assert_ne!(schema_hash, allow_any);
3481    }
3482
3483    #[test]
3484    fn open_object_and_closed_object_hash_differently_for_the_same_fields() {
3485        let mk_open = || {
3486            let mut b = Builder::new(opts());
3487            let x = b.boolean().unwrap();
3488            let n = b
3489                .open_object(
3490                    vec![("a".into(), x)],
3491                    &[true],
3492                    Vec::new(),
3493                    AdditionalPolicy::Forbid,
3494                    None,
3495                    None,
3496                    None,
3497                )
3498                .unwrap();
3499            b.finish(n).unwrap().canonical_hash()
3500        };
3501        let mk_closed = || {
3502            let mut b = Builder::new(opts());
3503            let x = b.boolean().unwrap();
3504            let n = b
3505                .object(vec![("a".into(), x)], &[true], ObjectClosure::Forbidden)
3506                .unwrap();
3507            b.finish(n).unwrap().canonical_hash()
3508        };
3509        assert_ne!(mk_open(), mk_closed());
3510    }
3511
3512    #[test]
3513    fn open_object_pattern_and_property_names_round_trip_through_wire() {
3514        let mut b = Builder::new(opts());
3515        let known_val = b.boolean().unwrap();
3516        let pattern_val = b.integer(None, None).unwrap();
3517        let names_schema = b
3518            .string_pattern("[a-z]+", None, None, Charset::Utf8Any)
3519            .unwrap();
3520        let n = b
3521            .open_object(
3522                vec![("a".into(), known_val)],
3523                &[true],
3524                vec![("^x_".into(), pattern_val)],
3525                AdditionalPolicy::AllowAny,
3526                Some(names_schema),
3527                Some(1),
3528                Some(10),
3529            )
3530            .unwrap();
3531        let ir = b.finish(n).unwrap();
3532        let bytes = ir.to_wire();
3533        let decoded = SchemaIR::from_wire(&bytes).unwrap();
3534        assert_eq!(ir.canonical_hash(), decoded.canonical_hash());
3535        let Node::OpenObject {
3536            min_properties,
3537            max_properties,
3538            property_names,
3539            ..
3540        } = decoded.node(decoded.root()).unwrap()
3541        else {
3542            panic!("expected OpenObject");
3543        };
3544        assert_eq!(*min_properties, Some(1));
3545        assert_eq!(*max_properties, Some(10));
3546        assert!(property_names.is_some());
3547    }
3548
3549    #[test]
3550    fn dependent_required_canonical_hash_is_unordered_and_wire_stable() {
3551        let options = CompileOptions::default();
3552        let first = crate::frontend::schema_to_ir(
3553            r#"{"dependentRequired":{"a":["b","c"],"x":["y"]}}"#,
3554            options.clone(),
3555        )
3556        .unwrap();
3557        let second = crate::frontend::schema_to_ir(
3558            r#"{"dependentRequired":{"x":["y"],"a":["c","b"]}}"#,
3559            options,
3560        )
3561        .unwrap();
3562        assert_eq!(IR_VERSION, 6);
3563        assert_eq!(first.canonical_hash(), second.canonical_hash());
3564        let encoded = first.to_wire();
3565        let decoded = SchemaIR::from_wire(&encoded).unwrap();
3566        assert_eq!(decoded, first);
3567        assert_eq!(decoded.canonical_hash(), first.canonical_hash());
3568        let mut v2 = encoded;
3569        v2[6..8].copy_from_slice(&2u16.to_le_bytes());
3570        assert_eq!(
3571            SchemaIR::from_wire(&v2).unwrap_err().code,
3572            ErrorCode::Malformed
3573        );
3574    }
3575
3576    #[test]
3577    fn decimal_comparison_is_exact_at_coefficient_and_scale_limits() {
3578        use std::cmp::Ordering;
3579
3580        assert_eq!(decimal_cmp((1, u32::MAX), (2, u32::MAX)), Ordering::Less);
3581        assert_eq!(
3582            decimal_cmp((-1, u32::MAX), (-2, u32::MAX)),
3583            Ordering::Greater
3584        );
3585        assert_eq!(decimal_cmp((10, 2), (1, 1)), Ordering::Equal);
3586        assert_eq!(decimal_cmp((i128::MAX, 38), (1, 0)), Ordering::Greater);
3587        assert_eq!(decimal_cmp((i128::MIN, 38), (-1, 0)), Ordering::Less);
3588    }
3589
3590    #[test]
3591    fn forged_dependent_required_arena_is_rejected_without_panicking() {
3592        let valid = crate::frontend::schema_to_ir(
3593            r#"{"type":"object","additionalProperties":true,"dependentRequired":{"a":["b"]}}"#,
3594            CompileOptions::default(),
3595        )
3596        .unwrap();
3597        let root = valid.root().get() as usize;
3598        let mut bad_slice = valid.to_wire_struct();
3599        match &mut bad_slice.nodes[root] {
3600            Node::OpenObject {
3601                dependent_required, ..
3602            }
3603            | Node::Object {
3604                dependent_required, ..
3605            } => dependent_required.off = u32::MAX,
3606            _ => panic!("expected object branch"),
3607        }
3608        assert_eq!(
3609            SchemaIR::assemble(bad_slice).unwrap_err().code,
3610            ErrorCode::Malformed
3611        );
3612
3613        for (off, len) in [(0, u32::MAX), (u32::MAX, u32::MAX)] {
3614            let mut bad_range = valid.to_wire_struct();
3615            match &mut bad_range.nodes[root] {
3616                Node::OpenObject {
3617                    dependent_required, ..
3618                }
3619                | Node::Object {
3620                    dependent_required, ..
3621                } => {
3622                    dependent_required.off = off;
3623                    dependent_required.len = len;
3624                }
3625                _ => unreachable!(),
3626            }
3627            assert_eq!(
3628                SchemaIR::assemble(bad_range).unwrap_err().code,
3629                ErrorCode::Malformed
3630            );
3631        }
3632
3633        for trigger in [true, false] {
3634            let mut bad_name = valid.to_wire_struct();
3635            let bad = StrRef {
3636                off: u32::MAX,
3637                len: 1,
3638            };
3639            if trigger {
3640                bad_name.dependent_required_pairs[0].trigger = bad;
3641            } else {
3642                bad_name.dependent_required_pairs[0].required = bad;
3643            }
3644            assert_eq!(
3645                SchemaIR::assemble(bad_name).unwrap_err().code,
3646                ErrorCode::Malformed
3647            );
3648        }
3649
3650        let mut duplicate = valid.to_wire_struct();
3651        duplicate
3652            .dependent_required_pairs
3653            .push(duplicate.dependent_required_pairs[0]);
3654        match &mut duplicate.nodes[root] {
3655            Node::OpenObject {
3656                dependent_required, ..
3657            }
3658            | Node::Object {
3659                dependent_required, ..
3660            } => dependent_required.len = 2,
3661            _ => unreachable!(),
3662        }
3663        assert_eq!(
3664            SchemaIR::assemble(duplicate).unwrap_err().code,
3665            ErrorCode::Malformed
3666        );
3667
3668        let mut over_cap = valid.to_wire_struct();
3669        over_cap.dependent_required_pairs =
3670            vec![over_cap.dependent_required_pairs[0]; MAX_DEPENDENT_REQUIRED_PAIRS + 1];
3671        assert_eq!(
3672            SchemaIR::assemble(over_cap).unwrap_err().code,
3673            ErrorCode::Malformed
3674        );
3675    }
3676}