1use crate::automaton::{
6 build_from_regex, concatenate, multiple_of_engine, repeat, string_set, union, Combinator,
7 ProductAutomaton, RefEngine, ShuffleAutomaton,
8};
9#[cfg(test)]
10use crate::automaton::{decimal_multiple_of_regex, multiple_of_regex};
11use crate::diagnostics::{Diagnostic, UnsupportedReason};
12use crate::error::{CompileError, ErrorCode, Stage};
13use crate::ir::{AdditionalPolicy, Charset, ItemsPolicy, Node, ScalarLit, SchemaIR};
14use crate::primitives::NodeId;
15
16#[derive(Clone, Copy, Debug, Eq, PartialEq)]
18#[repr(u8)]
19pub enum CompilePhase {
20 InputLoad,
22 JsonParse,
24 ResourceIndex,
26 AnchorIndex,
28 ReferenceResolution,
30 IrLowering,
32 RouteAnalysis,
34 RegexParse,
36 ThompsonNfa,
38 Determinization,
40 DfaMaterialization,
42 ByteClassConstruction,
44 StructuredAnalysis,
46 StructuredNodeBuild,
48 SccResolution,
50 PlanFreeze,
52 SchemaCacheLookup,
54 TrieLookup,
56 VocabularyBind,
58 PackedMaskFill,
60}
61
62impl CompilePhase {
63 pub const COUNT: usize = 20;
65}
66
67#[derive(Clone, Debug, Default, Eq, PartialEq)]
69pub struct PhaseDurations {
70 values: [u64; CompilePhase::COUNT],
71}
72
73impl PhaseDurations {
74 #[must_use]
76 pub fn get(&self, phase: CompilePhase) -> u64 {
77 self.values[phase as usize]
78 }
79
80 #[cfg(feature = "bench-internals")]
81 fn set(&mut self, phase: CompilePhase, value: u64) {
82 self.values[phase as usize] = value;
83 }
84}
85
86#[derive(Clone, Debug, Default, Eq, PartialEq)]
88pub struct CompileCounters {
89 pub schema_bytes: u64,
91 pub resource_bytes: u64,
93 pub ir_nodes: u64,
95 pub ir_edges: u64,
97 pub regular_nodes: u64,
99 pub structured_nodes: u64,
101 pub dfa_states: u64,
103 pub byte_classes: u64,
105 pub graph_edges: u64,
107 pub transition_cells: u64,
109 pub structured_plan_nodes: u64,
111 pub scc_nodes: u64,
113}
114
115#[derive(Clone, Debug, Default, Eq, PartialEq)]
117pub struct RetainedBytes {
118 pub ir: u64,
120 pub automaton: u64,
122 pub plan: u64,
124 pub estimated_peak_temporary: u64,
126}
127
128#[derive(Clone, Debug, Eq, PartialEq)]
130pub struct CompileProfile {
131 pub durations_ns: PhaseDurations,
133 pub counters: CompileCounters,
135 pub retained: RetainedBytes,
137 pub route: crate::routing::ExecutionKind,
139}
140
141const MAX_ARRAY_BODY_BYTES: usize = 1 << 16;
144
145fn require_regex_fits(len: usize) -> Result<(), CompileError> {
146 if len > MAX_ARRAY_BODY_BYTES {
147 return Err(CompileError::new(
148 ErrorCode::Unsupported,
149 Stage::L3,
150 UnsupportedReason::StructuredBackendRequired.advisory(),
151 ));
152 }
153 Ok(())
154}
155
156pub fn compile_ir(ir: &SchemaIR) -> Result<RefEngine, CompileError> {
160 if let Some(d) = ir.diagnostics().next() {
161 return Err(diagnostic_error(ir, d));
162 }
163 if ir.requires_structured_backend() {
164 return Err(CompileError::new(
165 ErrorCode::Unsupported,
166 Stage::L3,
167 UnsupportedReason::StructuredBackendRequired.advisory(),
168 ));
169 }
170 compile_node(ir, ir.root())
171}
172
173#[cfg(feature = "bench-internals")]
175pub fn compile_ir_profiled(ir: &SchemaIR) -> Result<(RefEngine, CompileProfile), CompileError> {
176 if let Some(d) = ir.diagnostics().next() {
177 return Err(diagnostic_error(ir, d));
178 }
179 let mut durations_ns = PhaseDurations::default();
180 let route_started = std::time::Instant::now();
181 let route_table = ir.route_table();
182 let route = route_table
183 .get(ir.root())
184 .map_or(crate::routing::ExecutionKind::Structured, |route| {
185 route.kind
186 });
187 durations_ns.set(
188 CompilePhase::RouteAnalysis,
189 u64::try_from(route_started.elapsed().as_nanos()).unwrap_or(u64::MAX),
190 );
191 if route != crate::routing::ExecutionKind::Regular {
192 return Err(CompileError::new(
193 ErrorCode::Unsupported,
194 Stage::L3,
195 UnsupportedReason::StructuredBackendRequired.advisory(),
196 ));
197 }
198 let started = std::time::Instant::now();
199 let engine = compile_node(ir, ir.root())?;
200 durations_ns.set(
201 CompilePhase::DfaMaterialization,
202 u64::try_from(started.elapsed().as_nanos()).unwrap_or(u64::MAX),
203 );
204 let regular_nodes = route_table
205 .routes()
206 .iter()
207 .filter(|node| node.kind == crate::routing::ExecutionKind::Regular)
208 .count();
209 let counters = CompileCounters {
210 ir_nodes: u64::try_from(ir.node_count()).unwrap_or(u64::MAX),
211 regular_nodes: u64::try_from(regular_nodes).unwrap_or(u64::MAX),
212 structured_nodes: u64::try_from(ir.node_count().saturating_sub(regular_nodes))
213 .unwrap_or(u64::MAX),
214 dfa_states: u64::try_from(engine.state_count()).unwrap_or(u64::MAX),
215 byte_classes: u64::try_from(engine.byte_class_count()).unwrap_or(u64::MAX),
216 graph_edges: u64::try_from(engine.live_transition_count()).unwrap_or(u64::MAX),
217 transition_cells: u64::try_from(engine.transition_cells()).unwrap_or(u64::MAX),
218 ..CompileCounters::default()
219 };
220 let retained = RetainedBytes {
221 automaton: u64::try_from(engine.heap_bytes()).unwrap_or(u64::MAX),
222 ..RetainedBytes::default()
223 };
224 Ok((
225 engine,
226 CompileProfile {
227 durations_ns,
228 counters,
229 retained,
230 route,
231 },
232 ))
233}
234
235pub fn compile_ir_timed(ir: &SchemaIR) -> Result<(RefEngine, u64, u64), CompileError> {
237 if let Some(d) = ir.diagnostics().next() {
238 return Err(diagnostic_error(ir, d));
239 }
240 let route_started = std::time::Instant::now();
241 let route = ir.route_table().get(ir.root()).map(|route| route.kind);
242 let route_analysis_ns = u64::try_from(route_started.elapsed().as_nanos()).unwrap_or(u64::MAX);
243 if route != Some(crate::routing::ExecutionKind::Regular) {
244 return Err(CompileError::new(
245 ErrorCode::Unsupported,
246 Stage::L3,
247 UnsupportedReason::StructuredBackendRequired.advisory(),
248 ));
249 }
250 let started = std::time::Instant::now();
251 let engine = compile_node(ir, ir.root())?;
252 let materialization_ns = u64::try_from(started.elapsed().as_nanos()).unwrap_or(u64::MAX);
253 Ok((engine, route_analysis_ns, materialization_ns))
254}
255
256pub(crate) fn compile_node(ir: &SchemaIR, id: NodeId) -> Result<RefEngine, CompileError> {
259 let node = ir
260 .node(id)
261 .ok_or_else(|| internal("node id out of range"))?;
262 if !uses_direct_compilation(ir, node) {
263 let body = lower_reachable(ir, id)?;
264 return build_from_regex(&wrapped_regex(&body)?);
265 }
266 let body = compile_unwrapped(ir, id)?;
267 surround_engine(&body, JSON_WS_RE, JSON_WS_RE)
268}
269
270fn uses_direct_compilation(ir: &SchemaIR, node: &Node) -> bool {
271 match node {
272 Node::Integer {
273 multiple_of: Some(_),
274 ..
275 }
276 | Node::Intersection { .. }
277 | Node::ExactlyOne { .. }
278 | Node::Union { .. } => true,
279 Node::Enum { values } => ir
280 .lits_at(*values)
281 .is_some_and(|items| items.iter().all(|item| matches!(item, ScalarLit::Str(_)))),
282 Node::Object {
283 dependent,
284 dependent_required,
285 ..
286 } => {
287 ir.props_at(*dependent).is_some_and(<[_]>::is_empty)
288 && ir
289 .dependent_required_at(*dependent_required)
290 .is_some_and(<[_]>::is_empty)
291 }
292 Node::Array {
293 items: ItemsPolicy::Schema(_),
294 unique_items: false,
295 contains: None,
296 ..
297 }
298 | Node::Tuple {
299 unique_items: false,
300 contains: None,
301 ..
302 } => true,
303 _ => false,
304 }
305}
306
307fn wrapped_regex(body: &str) -> Result<String, CompileError> {
308 let capacity = JSON_WS_RE
309 .len()
310 .checked_mul(2)
311 .and_then(|bytes| bytes.checked_add(body.len()))
312 .ok_or_else(|| compile_allocation("wrapped regex length"))?;
313 let mut pattern = String::new();
314 pattern
315 .try_reserve_exact(capacity)
316 .map_err(|_| compile_allocation("wrapped regex allocation"))?;
317 pattern.push_str(JSON_WS_RE);
318 pattern.push_str(body);
319 pattern.push_str(JSON_WS_RE);
320 Ok(pattern)
321}
322
323fn compile_unwrapped(ir: &SchemaIR, id: NodeId) -> Result<RefEngine, CompileError> {
324 let node = ir
325 .node(id)
326 .ok_or_else(|| internal("node id out of range"))?;
327 match node {
328 Node::Integer {
329 minimum,
330 maximum,
331 multiple_of: Some(modulus),
332 } => compile_integer_multiple(*minimum, *maximum, *modulus),
333 Node::Intersection { branches } => compile_product_node(ir, *branches, Combinator::All),
334 Node::ExactlyOne { branches } => {
335 compile_product_node(ir, *branches, Combinator::ExactlyOne)
336 }
337 Node::Union { branches } => compile_union_node(ir, *branches),
338 Node::Enum { values }
339 if ir.lits_at(*values).is_some_and(|literals| {
340 literals
341 .iter()
342 .all(|item| matches!(item, ScalarLit::Str(_)))
343 }) =>
344 {
345 compile_string_enum(ir, *values)
346 }
347 Node::Object {
348 fields,
349 required,
350 dependent,
351 dependent_required,
352 ..
353 } if ir
354 .props_at(*dependent)
355 .is_some_and(|items| items.is_empty())
356 && ir
357 .dependent_required_at(*dependent_required)
358 .is_some_and(|items| items.is_empty()) =>
359 {
360 compile_object_node(ir, *fields, *required)
361 }
362 Node::Array {
363 items: ItemsPolicy::Schema(item),
364 min_items,
365 max_items,
366 unique_items: false,
367 contains: None,
368 ..
369 } => compile_array_node(ir, *item, *min_items, *max_items),
370 Node::Tuple {
371 prefix,
372 tail,
373 min_items,
374 max_items,
375 unique_items: false,
376 contains: None,
377 ..
378 } => compile_tuple_node(ir, *prefix, *tail, *min_items, *max_items),
379 _ => build_from_regex(&lower_reachable(ir, id)?),
380 }
381}
382
383fn compile_integer_multiple(
384 minimum: Option<i64>,
385 maximum: Option<i64>,
386 modulus: u64,
387) -> Result<RefEngine, CompileError> {
388 let range = build_from_regex(&integer_regex(minimum, maximum))?;
389 let multiple = multiple_of_engine(modulus)?;
390 ProductAutomaton::build(&[&range, &multiple], Combinator::All)?.into_engine()
391}
392
393fn compile_string_enum(
394 ir: &SchemaIR,
395 values: crate::ir::LitSlice,
396) -> Result<RefEngine, CompileError> {
397 let literals = ir.lits_at(values).ok_or_else(|| internal("enum ref"))?;
398 let mut strings = Vec::new();
399 strings
400 .try_reserve_exact(literals.len())
401 .map_err(|_| compile_allocation("enum string reference allocation"))?;
402 for literal in literals {
403 if let ScalarLit::Str(value) = literal {
404 strings.push(value.as_str());
405 }
406 }
407 RefEngine::from_graph(&string_set(&strings)?)
408}
409
410fn compile_product_node(
411 ir: &SchemaIR,
412 branches: crate::ir::RefSlice,
413 combinator: Combinator,
414) -> Result<RefEngine, CompileError> {
415 let ids = ir.refs_at(branches).ok_or_else(|| internal("branch ref"))?;
416 let mut engines = Vec::new();
417 engines
418 .try_reserve_exact(ids.len())
419 .map_err(|_| compile_allocation("product engine allocation"))?;
420 for &id in ids {
421 engines.push(compile_unwrapped(ir, id)?);
422 }
423 let refs = engine_refs(&engines)?;
424 ProductAutomaton::build(&refs, combinator)?.into_engine()
425}
426
427fn compile_union_node(
428 ir: &SchemaIR,
429 branches: crate::ir::RefSlice,
430) -> Result<RefEngine, CompileError> {
431 let ids = ir.refs_at(branches).ok_or_else(|| internal("branch ref"))?;
432 let mut engines = Vec::new();
433 engines
434 .try_reserve_exact(ids.len())
435 .map_err(|_| compile_allocation("union engine allocation"))?;
436 for &id in ids {
437 engines.push(compile_unwrapped(ir, id)?);
438 }
439 let refs = engine_refs(&engines)?;
440 RefEngine::from_graph(&union(&refs)?)
441}
442
443fn compile_object_node(
444 ir: &SchemaIR,
445 fields: crate::ir::PropSlice,
446 required: crate::ir::BitSlice,
447) -> Result<RefEngine, CompileError> {
448 let properties = ir.props_at(fields).ok_or_else(|| internal("object ref"))?;
449 let mut field_engines = Vec::new();
450 let mut required_flags = Vec::new();
451 field_engines
452 .try_reserve_exact(properties.len())
453 .map_err(|_| compile_allocation("field engine allocation"))?;
454 required_flags
455 .try_reserve_exact(properties.len())
456 .map_err(|_| compile_allocation("field flag allocation"))?;
457 for (index, (name, child_id)) in properties.iter().enumerate() {
458 let name = ir.str_at(*name).ok_or_else(|| internal("field name"))?;
459 let prefix = build_from_regex(&format!(
460 "{}{}",
461 escape_regex(&json_encode(name)),
462 colon_sep()
463 ))?;
464 let child = compile_unwrapped(ir, *child_id)?;
465 field_engines.push(concat_engine(&[&prefix, &child])?);
466 let index = u32::try_from(index).map_err(|_| internal("field index overflow"))?;
467 required_flags.push(ir.is_required(required, index));
468 }
469 let refs = engine_refs(&field_engines)?;
470 let separator = build_from_regex(&comma_sep())?;
471 let body = ShuffleAutomaton::build(&refs, &required_flags, &separator)?.into_engine()?;
472 surround_engine(
473 &body,
474 &format!(r"\{{{JSON_WS_RE}"),
475 &format!(r"{JSON_WS_RE}\}}"),
476 )
477}
478
479fn compile_array_node(
480 ir: &SchemaIR,
481 item: NodeId,
482 min_items: u32,
483 max_items: Option<u32>,
484) -> Result<RefEngine, CompileError> {
485 let item = compile_unwrapped(ir, item)?;
486 let comma = build_from_regex(&comma_sep())?;
487 let comma_item = concat_engine(&[&comma, &item])?;
488 let body = if max_items == Some(0) {
489 build_from_regex("")?
490 } else if min_items == 0 {
491 let tail_max = max_items.map(|max| max.saturating_sub(1));
492 let tail = RefEngine::from_graph(&repeat(&comma_item, 0, tail_max)?)?;
493 let nonempty = concat_engine(&[&item, &tail])?;
494 let empty = build_from_regex("")?;
495 RefEngine::from_graph(&union(&[&empty, &nonempty])?)?
496 } else {
497 let tail = RefEngine::from_graph(&repeat(
498 &comma_item,
499 min_items - 1,
500 max_items.map(|max| max - 1),
501 )?)?;
502 concat_engine(&[&item, &tail])?
503 };
504 surround_engine(
505 &body,
506 &format!(r"\[{JSON_WS_RE}"),
507 &format!(r"{JSON_WS_RE}\]"),
508 )
509}
510
511fn compile_tuple_node(
512 ir: &SchemaIR,
513 prefix: crate::ir::RefSlice,
514 tail: Option<NodeId>,
515 min_items: u32,
516 max_items: Option<u32>,
517) -> Result<RefEngine, CompileError> {
518 let ids = ir.refs_at(prefix).ok_or_else(|| internal("tuple ref"))?;
519 let mut prefix_engines = Vec::new();
520 prefix_engines
521 .try_reserve_exact(ids.len())
522 .map_err(|_| compile_allocation("tuple engine allocation"))?;
523 for &id in ids {
524 prefix_engines.push(compile_unwrapped(ir, id)?);
525 }
526 let tail_engine = tail.map(|id| compile_unwrapped(ir, id)).transpose()?;
527 let body = tuple_body_engine(&prefix_engines, tail_engine.as_ref(), min_items, max_items)?;
528 surround_engine(
529 &body,
530 &format!(r"\[{JSON_WS_RE}"),
531 &format!(r"{JSON_WS_RE}\]"),
532 )
533}
534
535fn tuple_body_engine(
536 prefix: &[RefEngine],
537 tail: Option<&RefEngine>,
538 min_items: u32,
539 max_items: Option<u32>,
540) -> Result<RefEngine, CompileError> {
541 let prefix_len = u32::try_from(prefix.len()).map_err(|_| internal("tuple length"))?;
542 let upper = max_items.unwrap_or(prefix_len);
543 let finite_upper = if tail.is_some() {
544 upper
545 } else {
546 upper.min(prefix_len)
547 };
548 let mut alternatives = Vec::new();
549 let finite_count = finite_upper
550 .checked_sub(min_items)
551 .and_then(|count| count.checked_add(1))
552 .unwrap_or(0);
553 alternatives
554 .try_reserve_exact(
555 usize::try_from(finite_count)
556 .map_err(|_| compile_allocation("tuple alternative count"))?
557 .saturating_add(usize::from(max_items.is_none() && tail.is_some())),
558 )
559 .map_err(|_| compile_allocation("tuple alternative allocation"))?;
560 for count in min_items..=finite_upper {
561 alternatives.push(tuple_exact_engine(prefix, tail, count)?);
562 }
563 if max_items.is_none() {
564 if let Some(tail) = tail {
565 let base_count = min_items.max(prefix_len);
566 let base = tuple_exact_engine(prefix, Some(tail), base_count)?;
567 let comma = build_from_regex(&comma_sep())?;
568 let comma_tail = concat_engine(&[&comma, tail])?;
569 let rest = RefEngine::from_graph(&repeat(&comma_tail, 0, None)?)?;
570 alternatives.push(concat_engine(&[&base, &rest])?);
571 }
572 }
573 let refs = engine_refs(&alternatives)?;
574 RefEngine::from_graph(&union(&refs)?)
575}
576
577fn tuple_exact_engine(
578 prefix: &[RefEngine],
579 tail: Option<&RefEngine>,
580 count: u32,
581) -> Result<RefEngine, CompileError> {
582 if count == 0 {
583 return build_from_regex("");
584 }
585 let comma = build_from_regex(&comma_sep())?;
586 let count = usize::try_from(count).map_err(|_| internal("tuple count"))?;
587 let mut sequence: Vec<&RefEngine> = Vec::new();
588 sequence
589 .try_reserve_exact(count.saturating_mul(2).saturating_sub(1))
590 .map_err(|_| compile_allocation("tuple sequence allocation"))?;
591 for index in 0..count {
592 if index > 0 {
593 sequence.push(&comma);
594 }
595 let item = prefix.get(index).or(tail).ok_or_else(|| {
596 CompileError::new(
597 ErrorCode::Malformed,
598 Stage::L3,
599 "tuple count exceeds closed tail",
600 )
601 })?;
602 sequence.push(item);
603 }
604 concat_engine(&sequence)
605}
606
607fn concat_engine(engines: &[&RefEngine]) -> Result<RefEngine, CompileError> {
608 RefEngine::from_graph(&concatenate(engines)?)
609}
610
611fn engine_refs(engines: &[RefEngine]) -> Result<Vec<&RefEngine>, CompileError> {
612 let mut refs = Vec::new();
613 refs.try_reserve_exact(engines.len())
614 .map_err(|_| compile_allocation("engine reference allocation"))?;
615 refs.extend(engines.iter());
616 Ok(refs)
617}
618
619fn compile_allocation(what: &'static str) -> CompileError {
620 CompileError::new(
621 ErrorCode::InternalLimitExceeded,
622 Stage::L3,
623 "compiler allocation limit",
624 )
625 .with_observed(what)
626}
627
628fn surround_engine(
629 engine: &RefEngine,
630 prefix: &str,
631 suffix: &str,
632) -> Result<RefEngine, CompileError> {
633 let prefix = build_from_regex(prefix)?;
634 let suffix = build_from_regex(suffix)?;
635 concat_engine(&[&prefix, engine, &suffix])
636}
637
638pub(crate) fn number_syntax_engine() -> Result<RefEngine, CompileError> {
639 build_from_regex(&format!("{JSON_WS_RE}{JSON_NUMBER_RE}{JSON_WS_RE}"))
640}
641
642fn lower_reachable(ir: &SchemaIR, root: NodeId) -> Result<String, CompileError> {
645 let reachable = reachable_from(ir, root)?;
646 let mut frag: Vec<String> = vec![String::new(); ir.node_count()];
647 for (i, node) in ir.nodes().enumerate() {
648 if reachable[i] {
649 frag[i] = lower_node(ir, node, &frag)?;
650 }
651 }
652 frag.into_iter()
653 .nth(root.get() as usize)
654 .ok_or_else(|| internal("root fragment missing"))
655}
656
657fn reachable_from(ir: &SchemaIR, root: NodeId) -> Result<Vec<bool>, CompileError> {
659 let mut reached = vec![false; ir.node_count()];
660 let mut stack = vec![root];
661 while let Some(id) = stack.pop() {
662 let idx = id.get() as usize;
663 if reached.get(idx).copied().unwrap_or(true) {
664 continue;
665 }
666 reached[idx] = true;
667 let node = ir
668 .node(id)
669 .ok_or_else(|| internal("node id out of range"))?;
670 stack.extend(node_children(ir, node)?);
671 }
672 Ok(reached)
673}
674
675pub(crate) fn node_children(ir: &SchemaIR, node: &Node) -> Result<Vec<NodeId>, CompileError> {
677 Ok(match node {
678 Node::Array {
679 items: ItemsPolicy::Schema(id),
680 ..
681 } => vec![*id],
682 Node::Array {
683 items: ItemsPolicy::AllowAny,
684 ..
685 } => Vec::new(),
686 Node::Tuple { prefix, tail, .. } => {
687 let mut ids = ir
688 .refs_at(*prefix)
689 .ok_or_else(|| internal("tuple ref"))?
690 .to_vec();
691 ids.extend(*tail);
692 ids
693 }
694 Node::Object { fields, .. } => ir
695 .props_at(*fields)
696 .ok_or_else(|| internal("object ref"))?
697 .iter()
698 .map(|(_, c)| *c)
699 .collect(),
700 Node::OpenObject {
701 known,
702 patterns,
703 additional,
704 property_names,
705 ..
706 } => {
707 let mut ids: Vec<NodeId> = ir
708 .props_at(*known)
709 .ok_or_else(|| internal("open object known ref"))?
710 .iter()
711 .map(|(_, c)| *c)
712 .collect();
713 ids.extend(
714 ir.props_at(*patterns)
715 .ok_or_else(|| internal("open object pattern ref"))?
716 .iter()
717 .map(|(_, c)| *c),
718 );
719 if let AdditionalPolicy::Schema(id) = additional {
720 ids.push(*id);
721 }
722 ids.extend(*property_names);
723 ids
724 }
725 Node::Union { branches }
726 | Node::Intersection { branches }
727 | Node::ExactlyOne { branches } => ir
728 .refs_at(*branches)
729 .ok_or_else(|| internal("branch ref"))?
730 .to_vec(),
731 Node::Not { inner } => vec![*inner],
732 Node::DynamicRef { initial_target, .. } => vec![*initial_target],
733 Node::Unevaluated {
734 scope, unevaluated, ..
735 } => vec![*scope, *unevaluated],
736 Node::Null
737 | Node::Boolean
738 | Node::Never
739 | Node::StringConst { .. }
740 | Node::StringPattern { .. }
741 | Node::Integer { .. }
742 | Node::Number { .. }
743 | Node::LexicalNumber { .. }
744 | Node::Enum { .. }
745 | Node::Ref { .. }
746 | Node::Unsupported { .. } => Vec::new(),
747 })
748}
749
750const JSON_NUMBER_RE: &str = r"-?(?:0|[1-9][0-9]*)(?:\.[0-9]+)?(?:[eE][+-]?[0-9]+)?";
753
754const JSON_WS_RE: &str = r"[\x20\x09\x0A\x0D]*";
756
757fn colon_sep() -> String {
759 format!("{JSON_WS_RE}:{JSON_WS_RE}")
760}
761
762fn comma_sep() -> String {
764 format!("{JSON_WS_RE},{JSON_WS_RE}")
765}
766
767fn lower_node(ir: &SchemaIR, node: &Node, frag: &[String]) -> Result<String, CompileError> {
768 Ok(match node {
769 Node::Null => "null".to_string(),
770 Node::Boolean => "(?:true|false)".to_string(),
771 Node::Never => r"[^\x00-\xff]".to_string(),
772 Node::StringConst { value } => {
773 let s = ir.str_at(*value).ok_or_else(|| internal("string ref"))?;
774 escape_regex(&json_encode(s))
775 }
776 Node::StringPattern {
777 regex,
778 min_len,
779 max_len,
780 charset,
781 } => {
782 let base = ir.str_at(*regex).ok_or_else(|| internal("pattern ref"))?;
783 let body = match charset {
784 Charset::AsciiPrintableNoQuoteBackslash | Charset::Utf8CountedCodepoints => {
785 format!("(?:{base}){}", quantifier(*min_len, *max_len))
786 }
787 Charset::Utf8Any => format!("(?:{base})"),
788 };
789 format!("\"{body}\"")
790 }
791 Node::Integer {
792 minimum,
793 maximum,
794 multiple_of,
795 } => {
796 if multiple_of.is_some() {
797 return Err(internal("multipleOf must use direct graph compilation"));
798 }
799 integer_regex(*minimum, *maximum)
800 }
801 Node::Number {
802 integer_only,
803 minimum,
804 maximum,
805 multiple_of,
806 } => {
807 if *integer_only || minimum.is_some() || maximum.is_some() || multiple_of.is_some() {
808 return Err(CompileError::new(
809 ErrorCode::Unsupported,
810 Stage::L3,
811 UnsupportedReason::StructuredBackendRequired.advisory(),
812 ));
813 }
814 JSON_NUMBER_RE.to_string()
815 }
816 Node::LexicalNumber { regex } => ir
817 .str_at(*regex)
818 .ok_or_else(|| internal("lexical number pattern"))?
819 .to_owned(),
820 Node::Enum { values } => {
821 let lits = ir.lits_at(*values).ok_or_else(|| internal("enum ref"))?;
822 let mut alts: Vec<String> = lits.iter().map(scalar_regex).collect();
823 alts.sort_by_key(|a| std::cmp::Reverse(a.len()));
825 format!("(?:{})", alts.join("|"))
826 }
827 Node::Array {
828 unique_items: true, ..
829 }
830 | Node::Array {
831 items: ItemsPolicy::AllowAny,
832 ..
833 }
834 | Node::Array {
835 contains: Some(_), ..
836 } => {
837 return Err(CompileError::new(
838 ErrorCode::Unsupported,
839 Stage::L3,
840 UnsupportedReason::StructuredBackendRequired.advisory(),
841 ));
842 }
843 Node::Array {
844 items: ItemsPolicy::Schema(id),
845 min_items,
846 max_items,
847 ..
848 } => {
849 let elem = child(frag, *id)?;
850 let body = array_body(elem, *min_items, *max_items);
851 require_regex_fits(body.len())?;
852 format!("\\[{JSON_WS_RE}{body}{JSON_WS_RE}\\]")
853 }
854 Node::Tuple {
855 unique_items: true, ..
856 }
857 | Node::Tuple {
858 contains: Some(_), ..
859 } => {
860 return Err(CompileError::new(
861 ErrorCode::Unsupported,
862 Stage::L3,
863 UnsupportedReason::StructuredBackendRequired.advisory(),
864 ));
865 }
866 Node::Tuple {
867 prefix,
868 tail,
869 min_items,
870 max_items,
871 ..
872 } => {
873 let ids = ir.refs_at(*prefix).ok_or_else(|| internal("tuple ref"))?;
874 let mut elems = Vec::with_capacity(ids.len());
875 for id in ids {
876 elems.push(child(frag, *id)?);
877 }
878 let tail = match tail {
879 Some(t) => Some(child(frag, *t)?),
880 None => None,
881 };
882 let body = tuple_body(&elems, tail, *min_items, *max_items);
883 require_regex_fits(body.len())?;
884 format!("\\[{JSON_WS_RE}{body}{JSON_WS_RE}\\]")
885 }
886 Node::Object { dependent: dep, .. } if ir.props_at(*dep).is_some_and(|d| !d.is_empty()) => {
887 return Err(CompileError::new(
888 ErrorCode::Unsupported,
889 Stage::L3,
890 UnsupportedReason::StructuredBackendRequired.advisory(),
891 ));
892 }
893 Node::Object {
894 dependent_required, ..
895 } if ir
896 .dependent_required_at(*dependent_required)
897 .is_some_and(|pairs| !pairs.is_empty()) =>
898 {
899 return Err(CompileError::new(
900 ErrorCode::Unsupported,
901 Stage::L3,
902 UnsupportedReason::StructuredBackendRequired.advisory(),
903 ));
904 }
905 Node::Object {
906 fields, required, ..
907 } => {
908 let _ = (fields, required, frag);
909 return Err(internal("object must use direct graph compilation"));
910 }
911 Node::OpenObject { .. } => {
912 return Err(CompileError::new(
913 ErrorCode::Unsupported,
914 Stage::L3,
915 UnsupportedReason::StructuredBackendRequired.advisory(),
916 ));
917 }
918 Node::Union { branches } => {
919 let _ = branches;
920 return Err(internal("union must use direct graph compilation"));
921 }
922 Node::Intersection { branches } => {
923 let _ = branches;
924 return Err(internal("intersection must use direct graph compilation"));
925 }
926 Node::ExactlyOne { branches } => {
927 let _ = branches;
928 return Err(internal("oneOf must use direct graph compilation"));
929 }
930 Node::Not { .. }
931 | Node::Ref { .. }
932 | Node::DynamicRef { .. }
933 | Node::Unevaluated { .. } => {
934 return Err(CompileError::new(
935 ErrorCode::Unsupported,
936 Stage::L3,
937 UnsupportedReason::StructuredBackendRequired.advisory(),
938 ));
939 }
940 Node::Unsupported { .. } => return Err(internal("unsupported node reached the compiler")),
941 })
942}
943
944#[cfg(test)]
946fn intersect_regex(a: &str, b: &str) -> Result<String, CompileError> {
947 let engines = [build_from_regex(a)?, build_from_regex(b)?];
948 let refs: Vec<&RefEngine> = engines.iter().collect();
949 ProductAutomaton::build(&refs, Combinator::All)?.to_regex()
950}
951
952fn child(frag: &[String], id: NodeId) -> Result<&str, CompileError> {
953 frag.get(id.get() as usize)
954 .map(String::as_str)
955 .ok_or_else(|| internal("child fragment missing"))
956}
957
958fn scalar_regex(lit: &ScalarLit) -> String {
959 match lit {
960 ScalarLit::Null => "null".to_string(),
961 ScalarLit::Bool(true) => "true".to_string(),
962 ScalarLit::Bool(false) => "false".to_string(),
963 ScalarLit::Int(i) => i.to_string(),
964 ScalarLit::Str(s) => escape_regex(&json_encode(s)),
965 ScalarLit::Json(s) => json_literal_regex(s),
968 }
969}
970
971fn json_literal_regex(s: &str) -> String {
974 let mut out = String::with_capacity(s.len() * 2);
975 let mut in_string = false;
976 let mut escaped = false;
977 for c in s.chars() {
978 if in_string {
979 push_escaped(&mut out, c);
980 if escaped {
981 escaped = false;
982 } else if c == '\\' {
983 escaped = true;
984 } else if c == '"' {
985 in_string = false;
986 }
987 continue;
988 }
989 match c {
990 '"' => {
991 in_string = true;
992 push_escaped(&mut out, c);
993 }
994 '{' | '}' | '[' | ']' | ':' | ',' => {
995 out.push_str(JSON_WS_RE);
996 push_escaped(&mut out, c);
997 out.push_str(JSON_WS_RE);
998 }
999 c => push_escaped(&mut out, c),
1000 }
1001 }
1002 out
1003}
1004
1005fn array_body(element: &str, min: u32, max: Option<u32>) -> String {
1007 let e = format!("(?:{element})");
1008 let c = comma_sep();
1009 match max {
1010 None if min == 0 => format!("(?:{e}(?:{c}{e})*)?"),
1011 None => format!("{e}(?:{c}{e}){{{},}}", min - 1),
1012 Some(0) => String::new(),
1013 Some(max) if min == 0 => format!("(?:{e}(?:{c}{e}){{0,{}}})?", max - 1),
1014 Some(max) => format!("{e}(?:{c}{e}){{{},{}}}", min - 1, max - 1),
1015 }
1016}
1017
1018fn tuple_body(elems: &[&str], tail: Option<&str>, min: u32, max: Option<u32>) -> String {
1021 let n = u32::try_from(elems.len()).expect("tuple prefix length fits u32");
1023 let at = |i: u32| -> String {
1024 let e = if i < n {
1025 elems[i as usize]
1026 } else {
1027 tail.unwrap_or("")
1028 };
1029 format!("(?:{e})")
1030 };
1031 let c = comma_sep();
1032 let join = |k: u32| -> String { (0..k).map(&at).collect::<Vec<_>>().join(&c) };
1034
1035 let mut alts: Vec<String> = Vec::new();
1036 match (tail, max) {
1037 (None, _) => {
1039 let hi = max.map_or(n, |m| m.min(n));
1040 for k in min..=hi {
1041 alts.push(join(k));
1042 }
1043 }
1044 (Some(_), Some(hi)) if hi < n => {
1048 for k in min..=hi {
1050 alts.push(join(k));
1051 }
1052 }
1053 (Some(t), Some(hi)) => {
1054 for k in min..n {
1055 alts.push(join(k));
1056 }
1057 let base = join(n);
1058 let tail_lo = min.saturating_sub(n);
1059 let tail_hi = hi - n;
1060 let rep = if tail_hi == 0 {
1061 String::new()
1062 } else {
1063 format!("(?:{c}(?:{t})){{{tail_lo},{tail_hi}}}")
1064 };
1065 alts.push(format!("{base}{rep}"));
1066 }
1067 (Some(t), None) => {
1070 let hi = min.max(n);
1071 for k in min..hi {
1072 alts.push(join(k));
1073 }
1074 let base = join(n);
1075 let tail_lo = min.saturating_sub(n);
1076 let rep = if tail_lo == 0 {
1077 format!("(?:{c}(?:{t}))*")
1078 } else {
1079 format!("(?:{c}(?:{t})){{{tail_lo},}}")
1080 };
1081 alts.push(format!("{base}{rep}"));
1082 }
1083 }
1084 alts.sort_by_key(|a| std::cmp::Reverse(a.len()));
1086 format!("(?:{})", alts.join("|"))
1087}
1088
1089fn quantifier(min: Option<u32>, max: Option<u32>) -> String {
1090 match (min, max) {
1091 (Some(a), Some(b)) => format!("{{{a},{b}}}"),
1092 (Some(a), None) => format!("{{{a},}}"),
1093 (None, Some(b)) => format!("{{0,{b}}}"),
1094 (None, None) => "*".to_string(),
1095 }
1096}
1097
1098fn json_encode(s: &str) -> String {
1100 let mut out = String::with_capacity(s.len());
1101 for c in s.chars() {
1102 match c {
1103 '"' => out.push_str("\\\""),
1104 '\\' => out.push_str("\\\\"),
1105 '\n' => out.push_str("\\n"),
1106 '\r' => out.push_str("\\r"),
1107 '\t' => out.push_str("\\t"),
1108 '\u{08}' => out.push_str("\\b"),
1109 '\u{0c}' => out.push_str("\\f"),
1110 c if (c as u32) < 0x20 => out.push_str(&format!("\\u{:04x}", c as u32)),
1111 c => out.push(c),
1112 }
1113 }
1114 out
1115}
1116
1117const REGEX_META: &[char] = &[
1118 '\\', '.', '+', '*', '?', '(', ')', '[', ']', '{', '}', '^', '$', '|',
1119];
1120
1121fn push_escaped(out: &mut String, c: char) {
1123 if REGEX_META.contains(&c) {
1124 out.push('\\');
1125 }
1126 out.push(c);
1127}
1128
1129fn escape_regex(s: &str) -> String {
1131 let mut out = String::with_capacity(s.len() + 2);
1132 out.push('"');
1133 for c in s.chars() {
1134 push_escaped(&mut out, c);
1135 }
1136 out.push('"');
1137 out
1138}
1139
1140fn diagnostic_error(ir: &SchemaIR, d: &Diagnostic) -> CompileError {
1141 let mut e = CompileError::new(ErrorCode::Unsupported, Stage::L2, d.reason.advisory());
1142 if let Some(k) = ir.str_at(d.keyword) {
1143 e = e.with_keyword(k.to_string());
1144 }
1145 if let Some(p) = ir.str_at(d.json_pointer) {
1146 e = e.with_pointer(p.to_string());
1147 }
1148 e
1149}
1150
1151fn internal(what: &'static str) -> CompileError {
1152 CompileError::new(ErrorCode::InternalLimitExceeded, Stage::L2, what)
1153}
1154
1155pub(crate) fn integer_regex(lo: Option<i64>, hi: Option<i64>) -> String {
1161 let lo = lo.map(i128::from);
1162 let hi = hi.map(i128::from);
1163 let mut alts: Vec<String> = Vec::new();
1164
1165 let neg_hi = hi.map_or(-1, |h| h.min(-1));
1167 let neg_present = neg_hi <= -1 && lo.is_none_or(|l| l <= neg_hi);
1168 if neg_present {
1169 let a = to_u64(-neg_hi);
1170 match lo {
1171 Some(l) => alts.push(format!("-{}", pos_range(a, to_u64(-l)))),
1172 None => alts.push(format!("-{}", pos_ge(a))),
1173 }
1174 }
1175
1176 if lo.is_none_or(|l| l <= 0) && hi.is_none_or(|h| h >= 0) {
1177 alts.push("0".to_string());
1178 }
1179
1180 let pos_lo = lo.map_or(1, |l| l.max(1));
1182 if hi.is_none_or(|h| pos_lo <= h) {
1183 match hi {
1184 Some(h) => alts.push(pos_range(to_u64(pos_lo), to_u64(h))),
1185 None => alts.push(pos_ge(to_u64(pos_lo))),
1186 }
1187 }
1188
1189 format!("(?:{})", alts.join("|"))
1190}
1191
1192#[derive(Copy, Clone, PartialEq, Eq)]
1195#[cfg(test)]
1196enum FracRule {
1197 Any,
1198 Zero,
1199 NonZero,
1200}
1201
1202#[cfg(test)]
1207fn frac_rule_for_min(v: i64, exclusive: bool) -> Option<FracRule> {
1208 if v < 0 {
1209 if exclusive {
1210 None
1211 } else {
1212 Some(FracRule::Zero)
1213 }
1214 } else if exclusive {
1215 Some(FracRule::NonZero)
1216 } else {
1217 Some(FracRule::Any)
1218 }
1219}
1220
1221#[cfg(test)]
1223fn frac_rule_for_max(v: i64, exclusive: bool) -> Option<FracRule> {
1224 if v >= 0 {
1225 if exclusive {
1226 None
1227 } else {
1228 Some(FracRule::Zero)
1229 }
1230 } else if exclusive {
1231 Some(FracRule::NonZero)
1232 } else {
1233 Some(FracRule::Any)
1234 }
1235}
1236
1237#[cfg(test)]
1239fn combine_frac_rules(a: FracRule, b: FracRule) -> Option<FracRule> {
1240 match (a, b) {
1241 (FracRule::Zero, FracRule::NonZero) | (FracRule::NonZero, FracRule::Zero) => None,
1242 (FracRule::Zero, _) | (_, FracRule::Zero) => Some(FracRule::Zero),
1243 (FracRule::NonZero, _) | (_, FracRule::NonZero) => Some(FracRule::NonZero),
1244 (FracRule::Any, FracRule::Any) => Some(FracRule::Any),
1245 }
1246}
1247
1248#[cfg(test)]
1249fn frac_pattern(rule: FracRule) -> &'static str {
1250 match rule {
1251 FracRule::Any => r"(?:\.[0-9]+)?",
1252 FracRule::Zero => r"(?:\.0+)?",
1253 FracRule::NonZero => r"\.[0-9]*[1-9][0-9]*",
1254 }
1255}
1256
1257#[cfg(test)]
1260pub(crate) fn number_bounds_to_integer(
1261 minimum: Option<(i64, bool)>,
1262 maximum: Option<(i64, bool)>,
1263) -> (Option<i64>, Option<i64>) {
1264 let lo = minimum.map(|(v, excl)| if excl { v.saturating_add(1) } else { v });
1265 let hi = maximum.map(|(v, excl)| if excl { v.saturating_sub(1) } else { v });
1266 (lo, hi)
1267}
1268
1269#[cfg(test)]
1274pub(crate) fn number_multiple_of_regex(
1275 minimum: Option<(i64, bool)>,
1276 maximum: Option<(i64, bool)>,
1277 coef: u64,
1278 exp: u32,
1279) -> Result<String, CompileError> {
1280 if exp == 0 {
1281 let (lo, hi) = number_bounds_to_integer(minimum, maximum);
1282 let ints = intersect_regex(&integer_regex(lo, hi), &multiple_of_regex(coef)?)?;
1283 return Ok(format!("(?:{ints})(?:\\.0+)?"));
1284 }
1285 let range = number_range_regex(minimum, maximum);
1286 intersect_regex(&range, &decimal_multiple_of_regex(coef, exp)?)
1287}
1288
1289#[cfg(test)]
1294pub(crate) fn number_range_regex(
1295 minimum: Option<(i64, bool)>,
1296 maximum: Option<(i64, bool)>,
1297) -> String {
1298 if minimum.is_none() && maximum.is_none() {
1299 return JSON_NUMBER_RE.to_string();
1300 }
1301 let mut alts: Vec<String> = Vec::new();
1302 let zero_reachable = minimum
1303 .is_none_or(|(value, exclusive)| value < 0 || value == 0 && !exclusive)
1304 && maximum.is_none_or(|(value, exclusive)| value > 0 || value == 0 && !exclusive);
1305 if zero_reachable {
1306 alts.push(r"-0(?:\.0+)?".to_string());
1307 }
1308 let interior_lo = minimum.map(|(v, _)| v.checked_add(1));
1309 let interior_hi = maximum.map(|(v, _)| v.checked_sub(1));
1310 let interior_ok = match (interior_lo, interior_hi) {
1311 (Some(None), _) | (_, Some(None)) => false,
1312 (Some(Some(lo)), Some(Some(hi))) => lo <= hi,
1313 _ => true,
1314 };
1315 if interior_ok {
1316 let lo = interior_lo.flatten();
1317 let hi = interior_hi.flatten();
1318 alts.push(format!(
1319 "{}{}",
1320 integer_regex(lo, hi),
1321 frac_pattern(FracRule::Any)
1322 ));
1323 }
1324 let mut push_boundary = |v: i64, rule: Option<FracRule>| {
1325 if let Some(rule) = rule {
1326 alts.push(format!(
1327 "{}{}",
1328 integer_regex(Some(v), Some(v)),
1329 frac_pattern(rule)
1330 ));
1331 }
1332 };
1333 match (minimum, maximum) {
1334 (Some((mv, mex)), Some((xv, xex))) if mv == xv => {
1335 let rule = frac_rule_for_min(mv, mex).zip(frac_rule_for_max(xv, xex));
1336 if let Some((a, b)) = rule {
1337 push_boundary(mv, combine_frac_rules(a, b));
1338 }
1339 }
1340 _ => {
1341 if let Some((mv, mex)) = minimum {
1342 push_boundary(mv, frac_rule_for_min(mv, mex));
1343 }
1344 if let Some((xv, xex)) = maximum {
1345 push_boundary(xv, frac_rule_for_max(xv, xex));
1346 }
1347 }
1348 }
1349 let neg_zero_reachable =
1352 minimum.is_none_or(|(v, _)| v <= -1) && maximum.is_none_or(|(v, _)| v >= 0);
1353 if neg_zero_reachable {
1354 alts.push(format!("-0{}", frac_pattern(FracRule::NonZero)));
1355 }
1356 format!("(?:{})", alts.join("|"))
1357}
1358
1359fn to_u64(v: i128) -> u64 {
1361 u64::try_from(v).expect("integer bound magnitude fits u64")
1362}
1363
1364fn pow10(k: u32) -> u64 {
1365 let mut v = 1u64;
1366 for _ in 0..k {
1367 v *= 10;
1368 }
1369 v
1370}
1371
1372fn num_digits(n: u64) -> u32 {
1373 let mut d = 1;
1374 let mut v = n;
1375 while v >= 10 {
1376 v /= 10;
1377 d += 1;
1378 }
1379 d
1380}
1381
1382#[cfg(test)]
1384fn frac_str(a: &[u8]) -> String {
1385 a.iter().map(|d| (b'0' + d) as char).collect()
1386}
1387
1388#[cfg(test)]
1390fn strip_trailing_zeros(a: &[u8]) -> &[u8] {
1391 let mut end = a.len();
1392 while end > 0 && a[end - 1] == 0 {
1393 end -= 1;
1394 }
1395 &a[..end]
1396}
1397
1398#[cfg(test)]
1401fn frac_ge_body(a: &[u8]) -> String {
1402 let a = strip_trailing_zeros(a);
1403 if a.is_empty() {
1404 return "[0-9]+".to_string();
1405 }
1406 let mut alts: Vec<String> = Vec::new();
1407 for i in 0..a.len() {
1408 if a[i] < 9 {
1409 alts.push(format!("{}[{}-9][0-9]*", frac_str(&a[..i]), a[i] + 1));
1410 }
1411 }
1412 alts.push(format!("{}[0-9]*", frac_str(a)));
1413 format!("(?:{})", alts.join("|"))
1414}
1415
1416#[cfg(test)]
1418fn frac_le_body(a: &[u8]) -> String {
1419 let a = strip_trailing_zeros(a);
1420 if a.is_empty() {
1421 return "0+".to_string();
1422 }
1423 let mut alts: Vec<String> = Vec::new();
1424 for i in 0..a.len() {
1425 if a[i] > 0 {
1426 alts.push(format!("{}[0-{}][0-9]*", frac_str(&a[..i]), a[i] - 1));
1427 }
1428 }
1429 for i in 1..a.len() {
1430 alts.push(frac_str(&a[..i]));
1431 }
1432 alts.push(format!("{}0*", frac_str(a)));
1433 format!("(?:{})", alts.join("|"))
1434}
1435
1436#[cfg(test)]
1439fn lower_frac_suffix(lf: &[u8]) -> String {
1440 if strip_trailing_zeros(lf).is_empty() {
1441 return format!("(?:\\.{})?", frac_ge_body(lf));
1442 }
1443 format!("\\.{}", frac_ge_body(lf))
1444}
1445
1446#[cfg(test)]
1449fn upper_frac_suffix(hf: &[u8]) -> String {
1450 format!("(?:\\.{})?", frac_le_body(hf))
1451}
1452
1453#[cfg(test)]
1457fn nonneg_decimal_range(
1458 lo: (u64, &[u8]),
1459 hi: Option<(u64, &[u8])>,
1460) -> Result<String, CompileError> {
1461 let (li, lf) = lo;
1462 let li = li as i64;
1463 let mut alts: Vec<String> = Vec::new();
1464 match hi {
1465 None => {
1466 alts.push(format!(
1467 "{}{}",
1468 integer_regex(Some(li), None),
1469 lower_frac_suffix(lf)
1470 ));
1471 alts.push(format!(
1472 "{}(?:\\.[0-9]+)?",
1473 integer_regex(Some(li + 1), None)
1474 ));
1475 }
1476 Some((hi_i, hf)) => {
1477 let hi_i = hi_i as i64;
1478 if li == hi_i {
1479 let both = intersect_regex(&frac_ge_body(lf), &frac_le_body(hf))?;
1481 let frac = if strip_trailing_zeros(lf).is_empty() {
1482 format!("(?:\\.(?:{both}))?")
1483 } else {
1484 format!("\\.(?:{both})")
1485 };
1486 alts.push(format!("{}{}", integer_regex(Some(li), Some(li)), frac));
1487 } else {
1488 alts.push(format!(
1489 "{}{}",
1490 integer_regex(Some(li), Some(li)),
1491 lower_frac_suffix(lf)
1492 ));
1493 alts.push(format!(
1494 "{}{}",
1495 integer_regex(Some(hi_i), Some(hi_i)),
1496 upper_frac_suffix(hf)
1497 ));
1498 if hi_i - li >= 2 {
1499 alts.push(format!(
1500 "{}(?:\\.[0-9]+)?",
1501 integer_regex(Some(li + 1), Some(hi_i - 1))
1502 ));
1503 }
1504 }
1505 }
1506 }
1507 Ok(format!("(?:{})", alts.join("|")))
1508}
1509
1510#[cfg(test)]
1512fn to_decimal(bound: (i64, u32, bool)) -> (bool, u64, Vec<u8>) {
1513 let (value, scale, _) = bound;
1514 let mag = value.unsigned_abs();
1515 let div = 10u64.pow(scale);
1516 let frac_val = mag % div;
1517 let frac = format!("{frac_val:0width$}", width = scale as usize)
1518 .bytes()
1519 .map(|b| b - b'0')
1520 .collect();
1521 (value < 0, mag / div, frac)
1522}
1523
1524#[cfg(test)]
1526fn all_negatives() -> &'static str {
1527 r"-(?:[1-9][0-9]*(?:\.[0-9]+)?|0\.[0-9]*[1-9][0-9]*)"
1528}
1529
1530#[cfg(test)]
1531fn unsupported_decimal() -> CompileError {
1532 CompileError::new(
1533 ErrorCode::Unsupported,
1534 Stage::L3,
1535 "this decimal bound shape is not yet supported",
1536 )
1537}
1538
1539#[cfg(test)]
1543fn decimal_range_regex(
1544 minimum: Option<(i64, u32, bool)>,
1545 maximum: Option<(i64, u32, bool)>,
1546) -> Result<String, CompileError> {
1547 if minimum.is_some_and(|(_, _, e)| e) || maximum.is_some_and(|(_, _, e)| e) {
1548 return Err(unsupported_decimal());
1549 }
1550 let lo = minimum.map(to_decimal);
1551 let hi = maximum.map(to_decimal);
1552 match (&lo, &hi) {
1553 (Some((false, li, lf)), _) if hi.as_ref().is_none_or(|(hn, ..)| !hn) => {
1554 let hi_ref = hi.as_ref().map(|(_, hi_i, hf)| (*hi_i, hf.as_slice()));
1555 nonneg_decimal_range((*li, lf), hi_ref)
1556 }
1557 (None, Some((false, hi_i, hf))) => {
1558 let pos = nonneg_decimal_range((0, &[]), Some((*hi_i, hf.as_slice())))?;
1559 Ok(format!("(?:{}|{})", all_negatives(), pos))
1560 }
1561 _ => Err(unsupported_decimal()),
1562 }
1563}
1564
1565#[cfg(test)]
1568pub(crate) fn number_node_regex(
1569 minimum: Option<(i64, u32, bool)>,
1570 maximum: Option<(i64, u32, bool)>,
1571 multiple_of: Option<(u64, u32)>,
1572) -> Result<String, CompileError> {
1573 let as_int = |b: Option<(i64, u32, bool)>| match b {
1574 None => Some(None),
1575 Some((v, 0, e)) => Some(Some((v, e))),
1576 Some(_) => None,
1577 };
1578 if let (Some(lo), Some(hi)) = (as_int(minimum), as_int(maximum)) {
1579 return match multiple_of {
1580 None => Ok(number_range_regex(lo, hi)),
1581 Some((coef, exp)) => number_multiple_of_regex(lo, hi, coef, exp),
1582 };
1583 }
1584 if multiple_of.is_some() {
1585 return Err(unsupported_decimal());
1586 }
1587 decimal_range_regex(minimum, maximum)
1588}
1589
1590fn pos_ge(a: u64) -> String {
1593 let d = num_digits(a);
1594 let same = flrange(a, pow10(d) - 1, d);
1595 let more = if d == 1 {
1596 "[1-9][0-9]+".to_string()
1597 } else {
1598 format!("[1-9][0-9]{{{d},}}")
1599 };
1600 format!("(?:{more}|{same})")
1601}
1602
1603fn pos_range(a: u64, b: u64) -> String {
1606 let (da, db) = (num_digits(a), num_digits(b));
1607 if da == db {
1608 return flrange(a, b, da);
1609 }
1610 let mut parts = vec![flrange(a, pow10(da) - 1, da)];
1611 for d in (da + 1)..db {
1612 parts.push(all_digits(d));
1613 }
1614 parts.push(flrange(pow10(db - 1), b, db));
1615 parts.reverse(); alt(parts)
1617}
1618
1619fn all_digits(d: u32) -> String {
1621 if d == 1 {
1622 "[1-9]".to_string()
1623 } else {
1624 format!("[1-9][0-9]{{{}}}", d - 1)
1625 }
1626}
1627
1628fn flrange(lo: u64, hi: u64, len: u32) -> String {
1630 if lo == 0 && hi == pow10(len) - 1 {
1633 return any_digits(len);
1634 }
1635 if len == 1 {
1636 return digit_class(lo, hi);
1637 }
1638 let p = pow10(len - 1);
1639 let (ld, hd) = (lo / p, hi / p);
1640 let (lr, hr) = (lo % p, hi % p);
1641 if ld == hd {
1642 return format!("{}{}", digit(ld), flrange(lr, hr, len - 1));
1643 }
1644 let mut parts = vec![format!("{}{}", digit(ld), flrange(lr, p - 1, len - 1))];
1645 if hd >= ld + 2 {
1646 parts.push(format!(
1647 "{}{}",
1648 digit_class(ld + 1, hd - 1),
1649 any_digits(len - 1)
1650 ));
1651 }
1652 parts.push(format!("{}{}", digit(hd), flrange(0, hr, len - 1)));
1653 alt(parts)
1654}
1655
1656fn digit(d: u64) -> char {
1657 char::from(b'0' + u8::try_from(d).expect("a decimal digit is < 10"))
1659}
1660
1661fn digit_class(lo: u64, hi: u64) -> String {
1662 if lo == hi {
1663 digit(lo).to_string()
1664 } else {
1665 format!("[{}-{}]", digit(lo), digit(hi))
1666 }
1667}
1668
1669fn any_digits(k: u32) -> String {
1670 match k {
1671 0 => String::new(),
1672 1 => "[0-9]".to_string(),
1673 _ => format!("[0-9]{{{k}}}"),
1674 }
1675}
1676
1677fn alt(parts: Vec<String>) -> String {
1678 if parts.len() == 1 {
1679 parts.into_iter().next().unwrap_or_default()
1680 } else {
1681 format!("(?:{})", parts.join("|"))
1682 }
1683}
1684
1685#[cfg(test)]
1686mod tests {
1687 use super::*;
1688 use crate::automaton::build_from_regex;
1689 use crate::ir::{Builder, CompileOptions};
1690
1691 #[test]
1692 fn compile_ir_timed_fails_fast_on_a_structured_backend_schema_like_compile_ir() {
1693 let ir = crate::frontend::schema_to_ir(
1694 r#"{"type":"object","properties":{"a":{"type":"string"}},"additionalProperties":true}"#,
1695 CompileOptions::default(),
1696 )
1697 .unwrap();
1698 assert!(ir.requires_structured_backend());
1699 assert!(compile_ir(&ir).is_err());
1700 assert!(compile_ir_timed(&ir).is_err());
1701 }
1702
1703 fn frac_cmp(f: &[u8], a: &[u8]) -> std::cmp::Ordering {
1705 let n = f.len().max(a.len());
1706 let val = |d: &[u8]| -> u128 {
1707 let mut v = 0u128;
1708 for i in 0..n {
1709 v = v * 10 + u128::from(*d.get(i).unwrap_or(&0));
1710 }
1711 v
1712 };
1713 val(f).cmp(&val(a))
1714 }
1715
1716 fn digits(s: &str) -> Vec<u8> {
1717 s.bytes().map(|b| b - b'0').collect()
1718 }
1719
1720 #[test]
1721 fn frac_ge_le_bodies_agree_with_the_exact_rational_oracle() {
1722 let anchored = |body: &str| build_from_regex(&format!("^{body}$")).unwrap();
1723 for a_len in 1..=3usize {
1724 for a_code in 0..10u32.pow(a_len as u32) {
1725 let a = digits(&format!("{a_code:0a_len$}"));
1726 let ge = anchored(&frac_ge_body(&a));
1727 let le = anchored(&frac_le_body(&a));
1728 for f_len in 1..=4usize {
1729 for f_code in 0..10u32.pow(f_len as u32) {
1730 let f_str = format!("{f_code:0f_len$}");
1731 let f = digits(&f_str);
1732 let ord = frac_cmp(&f, &a);
1733 assert_eq!(
1734 ge.accepts(f_str.as_bytes()),
1735 ord != std::cmp::Ordering::Less,
1736 "ge a=0.{} f=0.{f_str}",
1737 frac_str(&a)
1738 );
1739 assert_eq!(
1740 le.accepts(f_str.as_bytes()),
1741 ord != std::cmp::Ordering::Greater,
1742 "le a=0.{} f=0.{f_str}",
1743 frac_str(&a)
1744 );
1745 }
1746 }
1747 }
1748 }
1749 }
1750
1751 fn scaled(x: &str, s: u32) -> i128 {
1753 let (i, f) = x.split_once('.').unwrap_or((x, ""));
1754 let mut digits = i.to_string();
1755 for _ in 0..s {
1756 digits.push('0');
1757 }
1758 let base: i128 = digits.parse().unwrap();
1759 let frac_shift: i128 = f
1760 .chars()
1761 .fold(0i128, |a, c| a * 10 + (c as u8 - b'0') as i128)
1762 * 10i128.pow(s - f.len() as u32);
1763 base + frac_shift
1764 }
1765
1766 #[test]
1767 fn nonneg_decimal_range_agrees_with_the_exact_rational_oracle() {
1768 let cands = [
1769 "0", "0.0", "0.5", "1", "1.0", "1.5", "1.50", "1.499", "1.7", "2", "2.5", "3", "3.7",
1770 "3.70", "3.700", "3.71", "0.25", "0.3", "2.999", "10", "10.4", "9.99", "0.1", "0.01",
1771 "1.25", "2.3", "2.7", "2.35",
1772 ];
1773 #[allow(clippy::type_complexity)]
1774 let bounds: &[((u64, &str), Option<(u64, &str)>)] = &[
1775 ((1, "5"), Some((3, "7"))),
1776 ((0, "0"), Some((1, "5"))),
1777 ((2, "3"), Some((2, "7"))),
1778 ((1, "5"), None),
1779 ((0, "25"), Some((10, "4"))),
1780 ((3, "0"), Some((3, "0"))),
1781 ];
1782 for (lo, hi) in bounds {
1783 let (li, lf) = (lo.0, digits(lo.1));
1784 let hi_pair = hi.map(|(i, f)| (i, digits(f)));
1785 let regex =
1786 nonneg_decimal_range((li, &lf), hi_pair.as_ref().map(|(i, f)| (*i, f.as_slice())))
1787 .unwrap();
1788 let e = build_from_regex(&format!("^{regex}$")).unwrap();
1789 let s = 6u32;
1790 let lo_v = scaled(&format!("{}.{}", lo.0, lo.1), s);
1791 let hi_v = hi.map(|(i, f)| scaled(&format!("{i}.{f}"), s));
1792 for c in cands {
1793 let v = scaled(c, s);
1794 let want = v >= lo_v && hi_v.is_none_or(|h| v <= h);
1795 assert_eq!(e.accepts(c.as_bytes()), want, "range={lo:?}..{hi:?} c={c}");
1796 }
1797 }
1798 }
1799
1800 #[test]
1801 fn never_is_dead_from_the_very_first_byte() {
1802 let mut b = Builder::new(CompileOptions::default());
1803 let n = b.never().unwrap();
1804 let ir = b.finish(n).unwrap();
1805 let e = compile_ir(&ir).unwrap();
1806 assert!(
1807 e.is_dead(e.start()),
1808 "never must be dead before consuming any byte"
1809 );
1810 assert!(!e.accepts(b"null"));
1811 assert!(!e.accepts(b"1"));
1812 assert!(!e.accepts(b""));
1813 }
1814
1815 #[test]
1817 fn single_required_field_object_skips_the_shuffle_round_trip() {
1818 let schema = r#"{"type":"object","additionalProperties":false,"required":["a"],
1819 "properties":{"a":{"type":"string"}}}"#;
1820 let ir = crate::frontend::schema_to_ir(schema, CompileOptions::default()).unwrap();
1821 let e = compile_ir(&ir).unwrap();
1822 assert!(e.accepts(br#"{"a":"x"}"#));
1823 assert!(!e.accepts(b"{}"), "the required field is missing");
1824 assert!(!e.accepts(br#"{"a":1}"#), "a is a string, not a number");
1825 }
1826
1827 #[test]
1828 fn single_optional_field_object_accepts_empty_or_present() {
1829 let schema = r#"{"type":"object","additionalProperties":false,
1830 "properties":{"a":{"type":"string"}}}"#;
1831 let ir = crate::frontend::schema_to_ir(schema, CompileOptions::default()).unwrap();
1832 let e = compile_ir(&ir).unwrap();
1833 assert!(e.accepts(br#"{"a":"x"}"#));
1834 assert!(e.accepts(b"{}"), "an optional field may be absent");
1835 }
1836
1837 #[test]
1841 fn a_real_world_nested_array_of_objects_schema_compiles_or_falls_back_in_under_a_second() {
1842 let schema = r#"{
1843 "properties": {
1844 "builders": {
1845 "items": {
1846 "properties": {
1847 "builder": {"pattern": ".*:.*", "type": "string"},
1848 "options": {"type": "object", "additionalProperties": true}
1849 },
1850 "required": ["builder"],
1851 "type": "object",
1852 "additionalProperties": true
1853 },
1854 "minItems": 1,
1855 "type": "array"
1856 }
1857 },
1858 "type": "object",
1859 "additionalProperties": true
1860 }"#;
1861 let opts = CompileOptions {
1862 object_closure: crate::ir::ObjectClosure::AssumeClosedProfile,
1863 ..CompileOptions::default()
1864 };
1865 let ir = crate::frontend::schema_to_ir(schema, opts).unwrap();
1866 assert_eq!(ir.diagnostics().next(), None);
1867 let start = std::time::Instant::now();
1868 let result = compile_ir(&ir);
1869 let elapsed = start.elapsed();
1870 assert!(
1871 elapsed < std::time::Duration::from_secs(1),
1872 "compile_ir must fail fast (or succeed fast), took {elapsed:?}"
1873 );
1874 if let Ok(engine) = result {
1876 assert!(engine.accepts(br#"{"builders":[{"builder":"a:b"}]}"#));
1877 } else {
1878 assert!(crate::structured::try_accepts(
1879 std::sync::Arc::new(ir),
1880 br#"{"builders":[{"builder":"a:b"}]}"#,
1881 )
1882 .expect("structured program"));
1883 }
1884 }
1885
1886 #[test]
1889 fn a_real_world_nested_array_of_array_of_one_of_schema_compiles_or_falls_back_fast() {
1890 let path = format!(
1892 "{}/../../integration/jsonschemabench/data/Github_easy/o67463.json",
1893 env!("CARGO_MANIFEST_DIR")
1894 );
1895 let Ok(schema) = std::fs::read_to_string(&path) else {
1896 eprintln!("skipping: corpus not present at {path}");
1897 return;
1898 };
1899 let opts = CompileOptions {
1900 object_closure: crate::ir::ObjectClosure::AssumeClosedProfile,
1901 ..CompileOptions::default()
1902 };
1903 let ir = crate::frontend::schema_to_ir(&schema, opts).unwrap();
1904 assert_eq!(ir.diagnostics().next(), None);
1905 let start = std::time::Instant::now();
1906 let result = compile_ir(&ir);
1907 let elapsed = start.elapsed();
1908 assert!(
1909 elapsed < std::time::Duration::from_secs(5),
1910 "compile_ir must fail fast (or succeed fast), took {elapsed:?}"
1911 );
1912 let data = br#"[[{"field":"year"}]]"#;
1913 if let Ok(engine) = result {
1914 assert!(engine.accepts(data));
1915 } else {
1916 assert!(
1917 crate::structured::try_accepts(std::sync::Arc::new(ir), data)
1918 .expect("structured program")
1919 );
1920 }
1921 }
1922
1923 fn engine_for_integer(lo: Option<i64>, hi: Option<i64>) -> RefEngine {
1924 let mut b = Builder::new(CompileOptions::default());
1925 let n = b.integer(lo, hi).unwrap();
1926 let ir = b.finish(n).unwrap();
1927 compile_ir(&ir).unwrap()
1928 }
1929
1930 #[test]
1931 fn integer_range_matches_membership_by_brute_force() {
1932 let bounds = [
1933 (Some(0), Some(0)),
1934 (Some(0), Some(9)),
1935 (Some(1), Some(255)),
1936 (Some(-5), Some(5)),
1937 (Some(-128), Some(127)),
1938 (Some(10), Some(1000)),
1939 (Some(-1000), Some(-7)),
1940 (Some(-50), Some(50)),
1941 (Some(99), Some(101)),
1942 (Some(7), Some(7)),
1943 (None, Some(3)),
1944 (Some(-3), None),
1945 (None, None),
1946 ];
1947 for (lo, hi) in bounds {
1948 let e = engine_for_integer(lo, hi);
1949 for n in -2000i64..=2000 {
1950 let want = lo.is_none_or(|l| n >= l) && hi.is_none_or(|h| n <= h);
1951 let got = e.accepts(n.to_string().as_bytes());
1952 assert_eq!(got, want, "n={n} bounds=({lo:?},{hi:?})");
1953 }
1954 }
1955 }
1956
1957 fn engine_for_tuple(
1958 prefix: Vec<(Option<i64>, Option<i64>)>,
1959 tail: Option<(Option<i64>, Option<i64>)>,
1960 min: u32,
1961 max: Option<u32>,
1962 ) -> RefEngine {
1963 let mut b = Builder::new(CompileOptions::default());
1964 let prefix_ids = prefix
1965 .iter()
1966 .map(|&(lo, hi)| b.integer(lo, hi).unwrap())
1967 .collect();
1968 let tail_id = tail.map(|(lo, hi)| b.integer(lo, hi).unwrap());
1969 let node = b.tuple(prefix_ids, tail_id, min, max).unwrap();
1970 let ir = b.finish(node).unwrap();
1971 compile_ir(&ir).unwrap()
1972 }
1973
1974 #[test]
1975 fn tuple_bounded_open_tail_accepts_exactly_the_valid_lengths() {
1976 let e = engine_for_tuple(
1978 vec![(Some(0), Some(9))],
1979 Some((Some(0), Some(9))),
1980 2,
1981 Some(4),
1982 );
1983 assert!(!e.accepts(b"[1]"), "below minItems");
1984 assert!(e.accepts(b"[1,2]"));
1985 assert!(e.accepts(b"[1,2,3]"));
1986 assert!(e.accepts(b"[1,2,3,4]"));
1987 assert!(!e.accepts(b"[1,2,3,4,5]"), "above maxItems");
1988 }
1989
1990 #[test]
1991 fn tuple_max_items_below_the_prefix_length_caps_within_the_prefix() {
1992 let e = engine_for_tuple(
1994 vec![(Some(0), Some(9)), (Some(0), Some(9)), (Some(0), Some(9))],
1995 Some((Some(0), Some(9))),
1996 1,
1997 Some(2),
1998 );
1999 assert!(e.accepts(b"[1]"));
2000 assert!(e.accepts(b"[1,2]"));
2001 assert!(
2002 !e.accepts(b"[1,2,3]"),
2003 "maxItems 2 is below the 3-position prefix"
2004 );
2005 }
2006
2007 #[test]
2008 fn tuple_moderate_max_items_compiles_without_a_quadratic_alternation() {
2009 let e = engine_for_tuple(
2012 vec![(Some(0), Some(9))],
2013 Some((Some(0), Some(9))),
2014 1,
2015 Some(64),
2016 );
2017 assert!(e.accepts(b"[1,2,3,4,5]"));
2018 assert!(e.accepts(b"[0]"));
2019 }
2020
2021 #[test]
2022 fn integer_extremes_compile_and_accept_the_boundary() {
2023 let e = engine_for_integer(Some(i64::MIN), Some(i64::MAX));
2024 assert!(e.accepts(i64::MIN.to_string().as_bytes()));
2025 assert!(e.accepts(i64::MAX.to_string().as_bytes()));
2026 assert!(e.accepts(b"0"));
2027 assert!(!e.accepts(b"-0"));
2028 assert!(!e.accepts(b"007"));
2029 }
2030
2031 #[test]
2032 fn rejects_leading_zero_and_plus() {
2033 let e = engine_for_integer(Some(0), Some(100));
2034 assert!(e.accepts(b"7"));
2035 assert!(!e.accepts(b"07"));
2036 assert!(!e.accepts(b"+7"));
2037 }
2038
2039 #[test]
2040 fn compiling_an_unsupported_ir_fails_with_the_diagnostic() {
2041 use crate::diagnostics::UnsupportedReason;
2042 let mut b = Builder::new(CompileOptions::default());
2043 let u = b
2044 .unsupported("anyOf", UnsupportedReason::UnionType, "/anyOf")
2045 .unwrap();
2046 let ir = b.finish(u).unwrap();
2047 let e = compile_ir(&ir).unwrap_err();
2048 assert_eq!(e.code, ErrorCode::Unsupported);
2049 assert_eq!(e.json_pointer_path.as_deref(), Some("/anyOf"));
2050 }
2051
2052 #[test]
2053 fn boolean_and_null_and_enum_compile() {
2054 let mut b = Builder::new(CompileOptions::default());
2055 let n = b.boolean().unwrap();
2056 let ir = b.finish(n).unwrap();
2057 let e = compile_ir(&ir).unwrap();
2058 assert!(e.accepts(b"true") && e.accepts(b"false") && !e.accepts(b"null"));
2059 }
2060
2061 #[test]
2062 fn large_string_enum_compiles_as_a_decoded_value_trie() {
2063 let mut values = (0..=crate::ir::MAX_UNROLLED_ENUM)
2064 .map(|index| ScalarLit::Str(format!("member-{index:04}")))
2065 .collect::<Vec<_>>();
2066 values.extend([
2067 ScalarLit::Str("a".to_owned()),
2068 ScalarLit::Str("😀".to_owned()),
2069 ]);
2070 let mut builder = Builder::new(CompileOptions::default());
2071 let root = builder.enum_values(values).unwrap();
2072 let ir = builder.finish(root).unwrap();
2073 assert!(!ir.requires_structured_backend());
2074 let engine = compile_ir(&ir).unwrap();
2075 assert!(engine.accepts(br#""member-0042""#));
2076 assert!(engine.accepts(br#""\u0061""#));
2077 assert!(engine.accepts(br#""\uD83D\uDE00""#));
2078 assert!(!engine.accepts(br#""member-9999""#));
2079 }
2080
2081 #[test]
2082 fn small_string_enum_uses_decoded_json_string_semantics() {
2083 let mut builder = Builder::new(CompileOptions::default());
2084 let root = builder
2085 .enum_values(vec![
2086 ScalarLit::Str("a".to_owned()),
2087 ScalarLit::Str("😀".to_owned()),
2088 ])
2089 .unwrap();
2090 let engine = compile_ir(&builder.finish(root).unwrap()).unwrap();
2091 for accepted in [br#""a""#.as_slice(), br#""\u0061""#, br#""\ud83d\ude00""#] {
2092 assert!(engine.accepts(accepted), "rejected {accepted:?}");
2093 }
2094 }
2095
2096 fn engine_for_union(ranges: &[(Option<i64>, Option<i64>)]) -> RefEngine {
2097 let mut b = Builder::new(CompileOptions::default());
2098 let branches = ranges
2099 .iter()
2100 .map(|&(lo, hi)| b.integer(lo, hi).unwrap())
2101 .collect();
2102 let u = b.union_of(branches).unwrap();
2103 let ir = b.finish(u).unwrap();
2104 compile_ir(&ir).unwrap()
2105 }
2106
2107 #[test]
2112 fn union_of_colliding_integer_ranges_matches_membership_by_brute_force() {
2113 let cases: &[&[(Option<i64>, Option<i64>)]] = &[
2114 &[(Some(0), Some(5)), (Some(3), Some(10))],
2115 &[(Some(0), Some(10)), (Some(5), Some(15))],
2116 &[(Some(-5), Some(5)), (Some(0), Some(10))],
2117 &[(Some(-100), Some(-7)), (Some(-20), Some(20))],
2118 ];
2119 for ranges in cases {
2120 let e = engine_for_union(ranges);
2121 for n in -200i64..=200 {
2122 let want = ranges
2123 .iter()
2124 .any(|&(lo, hi)| lo.is_none_or(|l| n >= l) && hi.is_none_or(|h| n <= h));
2125 let got = e.accepts(n.to_string().as_bytes());
2126 assert_eq!(got, want, "n={n} ranges={ranges:?}");
2127 }
2128 }
2129 }
2130
2131 #[test]
2132 fn union_of_0_5_and_3_10_accepts_10() {
2133 let e = engine_for_union(&[(Some(0), Some(5)), (Some(3), Some(10))]);
2134 assert!(
2135 e.accepts(b"10"),
2136 "10 is in [3,10] so the union must accept it"
2137 );
2138 assert!(e.accepts(b"0"));
2139 assert!(e.accepts(b"5"));
2140 assert!(!e.accepts(b"11"));
2141 }
2142
2143 #[test]
2144 fn direct_intersection_preserves_acceptance_and_outer_whitespace() {
2145 let schema = r#"{"allOf":[{"type":"integer","minimum":0,"maximum":10},{"type":"integer","minimum":5,"maximum":20}]}"#;
2146 let ir = crate::frontend::schema_to_ir(schema, CompileOptions::default()).unwrap();
2147 let engine = compile_ir(&ir).unwrap();
2148 for accepted in [b"5".as_slice(), b"10", b" \t5\r\n"] {
2149 assert!(engine.accepts(accepted), "rejected {accepted:?}");
2150 }
2151 for rejected in [b"4".as_slice(), b"11"] {
2152 assert!(!engine.accepts(rejected), "accepted {rejected:?}");
2153 }
2154 }
2155
2156 #[test]
2160 #[ignore]
2161 fn stage_breakdown_on_production_corpus_schemas() {
2162 use crate::frontend::schema_to_ir;
2163 use std::time::Instant;
2164
2165 let schemas: [(&str, &str); 6] = [
2166 ("bool", r#"{"type":"boolean"}"#),
2167 (
2168 "flat_object",
2169 r#"{"type":"object","properties":{"name":{"type":"string"},"age":{"type":"integer"}},"required":["name","age"],"additionalProperties":false}"#,
2170 ),
2171 (
2172 "nested_object",
2173 r#"{"type":"object","properties":{"user":{"type":"object","properties":{"id":{"type":"integer"},"active":{"type":"boolean"}},"required":["id","active"],"additionalProperties":false}},"required":["user"],"additionalProperties":false}"#,
2174 ),
2175 (
2176 "bounded_array_of_objects",
2177 r#"{"type":"array","items":{"type":"object","properties":{"x":{"type":"number"}},"required":["x"],"additionalProperties":false},"minItems":1,"maxItems":5}"#,
2178 ),
2179 (
2180 "deeply_nested",
2181 r#"{"type":"object","properties":{"l1":{"type":"object","properties":{"l2":{"type":"object","properties":{"l3":{"type":"boolean"}},"required":["l3"],"additionalProperties":false}},"required":["l2"],"additionalProperties":false}},"required":["l1"],"additionalProperties":false}"#,
2182 ),
2183 (
2184 "mixed_object_array",
2185 r#"{"type":"object","properties":{"tags":{"type":"array","items":{"type":"string"},"minItems":0,"maxItems":3},"count":{"type":"integer"}},"required":["tags","count"],"additionalProperties":false}"#,
2186 ),
2187 ];
2188
2189 for (name, text) in schemas {
2190 let n = 200;
2191 let mut frontend_ns = 0u128;
2192 let mut lower_ns = 0u128;
2193 let mut engine_ns = 0u128;
2194 for _ in 0..n {
2195 let t0 = Instant::now();
2196 let ir = schema_to_ir(text, crate::ir::CompileOptions::default()).unwrap();
2197 frontend_ns += t0.elapsed().as_nanos();
2198
2199 let t0 = Instant::now();
2200 let pattern = lower_reachable(&ir, ir.root()).unwrap();
2201 lower_ns += t0.elapsed().as_nanos();
2202
2203 let t0 = Instant::now();
2204 build_from_regex(&pattern).unwrap();
2205 engine_ns += t0.elapsed().as_nanos();
2206 }
2207 println!(
2208 "{name:28} frontend={:>8.1}us lower={:>8.1}us regex_to_engine={:>8.1}us",
2209 frontend_ns as f64 / n as f64 / 1000.0,
2210 lower_ns as f64 / n as f64 / 1000.0,
2211 engine_ns as f64 / n as f64 / 1000.0,
2212 );
2213 }
2214 }
2215
2216 #[test]
2220 fn exhaustive_small_range_pair_sweep_agrees_with_brute_force_membership() {
2221 let ranges: Vec<(i64, i64)> = (0i64..=8)
2222 .flat_map(|lo| (lo..=8).map(move |hi| (lo, hi)))
2223 .collect();
2224 for &(a_lo, a_hi) in &ranges {
2225 for &(b_lo, b_hi) in &ranges {
2226 let branches = [(Some(a_lo), Some(a_hi)), (Some(b_lo), Some(b_hi))];
2227 let e = engine_for_union(&branches);
2228 for n in 0i64..=15 {
2229 let want = (n >= a_lo && n <= a_hi) || (n >= b_lo && n <= b_hi);
2230 let got = e.accepts(n.to_string().as_bytes());
2231 assert_eq!(got, want, "n={n} a=[{a_lo},{a_hi}] b=[{b_lo},{b_hi}]");
2232 }
2233 }
2234 }
2235 }
2236
2237 #[test]
2241 fn number_range_brute_force_matches_independent_decimal_comparison() {
2242 for min_v in -3i64..=3 {
2243 for min_excl in [false, true] {
2244 for max_v in -3i64..=3 {
2245 for max_excl in [false, true] {
2246 if max_v < min_v || (max_v == min_v && (min_excl || max_excl)) {
2247 continue;
2248 }
2249 let pattern =
2250 number_range_regex(Some((min_v, min_excl)), Some((max_v, max_excl)));
2251 let e = build_from_regex(&pattern).unwrap();
2252 for sign in [1i64, -1i64] {
2253 for int_mag in 0i64..=5 {
2254 for has_frac in [false, true] {
2255 if sign < 0 && int_mag == 0 && !has_frac {
2256 continue;
2257 }
2258 let text = format!(
2259 "{}{int_mag}{}",
2260 if sign < 0 { "-" } else { "" },
2261 if has_frac { ".1" } else { "" }
2262 );
2263 let value = sign as f64
2264 * (int_mag as f64 + if has_frac { 0.1 } else { 0.0 });
2265 let ge_min = if min_excl {
2266 value > min_v as f64
2267 } else {
2268 value >= min_v as f64
2269 };
2270 let le_max = if max_excl {
2271 value < max_v as f64
2272 } else {
2273 value <= max_v as f64
2274 };
2275 let want = ge_min && le_max;
2276 let got = e.accepts(text.as_bytes());
2277 assert_eq!(
2278 got, want,
2279 "text={text} min=({min_v},{min_excl}) max=({max_v},{max_excl})"
2280 );
2281 }
2282 }
2283 }
2284 }
2285 }
2286 }
2287 }
2288 }
2289
2290 fn engine_from_schema(schema: &str) -> RefEngine {
2291 let ir = crate::frontend::schema_to_ir(schema, CompileOptions::default()).unwrap();
2292 compile_ir(&ir).unwrap()
2293 }
2294
2295 #[test]
2296 fn object_whitespace_variants_are_all_accepted() {
2297 let e = engine_from_schema(
2298 r#"{"type":"object","properties":{"a":{"type":"integer"}},"required":["a"],"additionalProperties":false}"#,
2299 );
2300 assert!(e.accepts(br#"{"a":1}"#));
2301 assert!(e.accepts(br#"{"a" : 1}"#));
2302 assert!(e.accepts(br#"{ "a" : 1 }"#));
2303 assert!(e.accepts(b"{\n\"a\"\t:\r1\n}"));
2304 }
2305
2306 #[test]
2307 fn array_whitespace_variants_are_all_accepted() {
2308 let e = engine_from_schema(r#"{"type":"array","items":{"type":"integer"}}"#);
2309 assert!(e.accepts(b"[1,2]"));
2310 assert!(e.accepts(b"[ 1 , 2 ]"));
2311 }
2312
2313 #[test]
2314 fn root_leading_and_trailing_whitespace_is_accepted() {
2315 let e = engine_from_schema(r#"{"type":"boolean"}"#);
2316 assert!(e.accepts(b" true"));
2317 assert!(e.accepts(b"true "));
2318 assert!(e.accepts(b" \t\ntrue\r\n "));
2319 }
2320
2321 #[test]
2322 fn nested_object_and_array_whitespace_is_accepted() {
2323 let e = engine_from_schema(
2324 r#"{"type":"object","properties":{"xs":{"type":"array","items":{"type":"boolean"}}},"required":["xs"],"additionalProperties":false}"#,
2325 );
2326 assert!(e.accepts(br#"{ "xs" : [ true , false ] }"#));
2327 }
2328
2329 #[test]
2330 fn punctuation_inside_a_string_const_is_never_treated_as_structure() {
2331 let e = engine_from_schema(r#"{"const":"a:b,c"}"#);
2332 assert!(e.accepts(br#""a:b,c""#));
2333 assert!(
2334 !e.accepts(br#""a: b, c""#),
2335 "string content is literal, not JSON structure"
2336 );
2337 }
2338
2339 #[test]
2340 fn composite_object_const_keeps_string_content_literal_but_own_structure_spaced() {
2341 let e = engine_from_schema(r#"{"const":{"a":"x:y,z"}}"#);
2342 assert!(e.accepts(br#"{"a":"x:y,z"}"#));
2343 assert!(e.accepts(br#"{ "a" : "x:y,z" }"#));
2344 assert!(
2345 !e.accepts(br#"{"a":"x: y, z"}"#),
2346 "the : and , inside the string are literal"
2347 );
2348 }
2349
2350 #[test]
2351 fn composite_literal_handles_escaped_quotes_and_backslashes() {
2352 let e = engine_from_schema(r#"{"const":{"k":"a\"b\\c"}}"#);
2353 assert!(e.accepts(br#"{"k":"a\"b\\c"}"#));
2354 assert!(e.accepts(br#"{ "k" : "a\"b\\c" }"#));
2355 }
2356
2357 #[test]
2358 fn composite_array_and_enum_const_accept_whitespace_around_their_own_structure() {
2359 let arr = engine_from_schema(r#"{"const":[1,{"a":true},[2,3]]}"#);
2360 assert!(arr.accepts(br#"[1,{"a":true},[2,3]]"#));
2361 assert!(arr.accepts(br#"[ 1 , { "a" : true } , [ 2 , 3 ] ]"#));
2362
2363 let en = engine_from_schema(r#"{"enum":[{"x":1},[2,3]]}"#);
2364 assert!(en.accepts(br#"{ "x" : 1 }"#));
2365 assert!(en.accepts(b"[ 2 , 3 ]"));
2366 }
2367}