1use super::*;
7
8pub(crate) fn check_type_decl(
9 t: &TypeDecl,
10 types: &HashMap<String, Arc<TypeDecl>>,
11 tys: &Types,
12 errors: &mut Vec<CompileError>,
13) {
14 match &t.body {
15 TypeBody::Refined {
16 base,
17 base_span,
18 refinement,
19 } => {
20 check_refinement(*base, *base_span, refinement.as_ref(), errors);
21 }
22 TypeBody::Opaque {
23 base,
24 base_span,
25 refinement,
26 } => {
27 check_refinement(*base, *base_span, refinement.as_ref(), errors);
29 }
30 TypeBody::Record(r) => {
31 for f in &r.fields {
32 if let Some(ref_r) = &f.refinement {
33 if let Some(b) = field_base_type(&f.type_ref, types) {
35 check_refinement(b, f.type_ref.span(), Some(ref_r), errors);
36 } else {
37 errors.push(CompileError::new(
38 "bynk.types.field_refinement_not_base",
39 ref_r.span,
40 format!(
41 "inline refinement on field `{}` requires a base or refined type",
42 f.name.name
43 ),
44 ));
45 }
46 }
47 if let Some(init) = &f.init {
58 errors.push(
59 CompileError::new(
60 "bynk.event.default_outside_event",
61 init.span,
62 format!(
63 "a field default is only meaningful on an `event`'s field, not `{}`",
64 f.name.name
65 ),
66 )
67 .with_note(
68 "a default exists so an older wire event missing this key can still \
69 deserialise — an ordinary record has no wire-evolution story, so a \
70 default here would be silently ignored",
71 ),
72 );
73 }
74 }
75 }
76 TypeBody::Sum(s) => {
77 check_embeds(&t.name.name, s, types, errors, tys);
78 }
79 }
80}
81
82pub(crate) fn check_embeds(
88 sum_name: &str,
89 s: &SumBody,
90 types: &HashMap<String, Arc<TypeDecl>>,
91 errors: &mut Vec<CompileError>,
92 tys: &Types,
93) {
94 let mut seen_sources: Vec<TyId> = Vec::new();
95 for clause in &s.embeds {
96 let Some(source_ty) = resolve_type_ref(&clause.source_type, types, tys) else {
97 continue;
100 };
101 let Some(variant) = s
103 .variants
104 .iter()
105 .find(|v| v.name.name == clause.variant.name)
106 else {
107 errors.push(CompileError::new(
108 "bynk.types.embeds_unknown_variant",
109 clause.variant.span,
110 format!(
111 "`embeds … as {}` names no variant of `{}`",
112 clause.variant.name, sum_name
113 ),
114 ));
115 continue;
116 };
117 if variant.payload.len() != 1 {
119 errors.push(
120 CompileError::new(
121 "bynk.types.embeds_variant_shape",
122 clause.span,
123 format!(
124 "`embeds … as {}` requires `{}` to have exactly one payload field, but it has {}",
125 clause.variant.name,
126 clause.variant.name,
127 variant.payload.len()
128 ),
129 )
130 .with_note("a value of the embedded type is wrapped into that single field"),
131 );
132 continue;
133 }
134 let field_ty = resolve_type_ref(&variant.payload[0].type_ref, types, tys);
135 if let Some(field_ty) = &field_ty
136 && !compatible(source_ty, *field_ty, tys)
137 {
138 errors.push(CompileError::new(
139 "bynk.types.embeds_variant_shape",
140 clause.span,
141 format!(
142 "`embeds {} as {}` — but `{}`'s payload field has type `{}`, not `{}`",
143 source_ty.display(tys),
144 clause.variant.name,
145 clause.variant.name,
146 field_ty.display(tys),
147 source_ty.display(tys)
148 ),
149 ));
150 continue;
151 }
152 if seen_sources.iter().any(|t| compatible(*t, source_ty, tys)) {
154 errors.push(CompileError::new(
155 "bynk.types.embeds_ambiguous",
156 clause.span,
157 format!(
158 "`{}` is embedded more than once by `{}` — the conversion would be ambiguous",
159 source_ty.display(tys),
160 sum_name
161 ),
162 ));
163 continue;
164 }
165 seen_sources.push(source_ty);
166 }
167}
168
169fn field_base_type(r: &TypeRef, types: &HashMap<String, Arc<TypeDecl>>) -> Option<BaseType> {
171 match r {
172 TypeRef::Base(b, _) => Some(*b),
173 TypeRef::Named(id) => match types.get(&id.name).map(|t| &t.body) {
174 Some(TypeBody::Refined { base, .. }) => Some(*base),
175 _ => None,
176 },
177 _ => None,
178 }
179}
180
181pub(crate) fn type_decl_base(decl: &TypeDecl) -> Option<BaseType> {
185 match &decl.body {
186 TypeBody::Refined { base, .. } => Some(*base),
187 TypeBody::Opaque { base, .. } => Some(*base),
188 _ => None,
189 }
190}
191
192pub(crate) fn type_decl_refinement(decl: &TypeDecl) -> Option<&Refinement> {
194 match &decl.body {
195 TypeBody::Refined { refinement, .. } | TypeBody::Opaque { refinement, .. } => {
196 refinement.as_ref()
197 }
198 _ => None,
199 }
200}
201
202pub(crate) fn const_literal(e: &Expr) -> Option<ConstLit> {
207 match &e.kind {
208 ExprKind::IntLit { value: n, .. } => Some(ConstLit::Int(*n)),
209 ExprKind::FloatLit { value, .. } => Some(ConstLit::Float(*value)),
210 ExprKind::StrLit(s) => Some(ConstLit::Str(s.clone())),
211 ExprKind::BoolLit(b) => Some(ConstLit::Bool(*b)),
212 ExprKind::UnitLit => Some(ConstLit::Unit),
213 ExprKind::UnaryOp(UnaryOp::Neg, inner) => match &inner.kind {
214 ExprKind::IntLit { value: n, .. } => Some(ConstLit::Int(n.checked_neg()?)),
215 ExprKind::FloatLit { value, .. } => Some(ConstLit::Float(-*value)),
216 _ => None,
217 },
218 _ => None,
219 }
220}
221
222pub(crate) fn eval_predicate(pred: &PredKind, lit: &ConstLit) -> bool {
229 match (pred, lit) {
230 (PredKind::NonNegative, ConstLit::Int(n)) => *n >= 0,
231 (PredKind::Positive, ConstLit::Int(n)) => *n > 0,
232 (PredKind::InRange(lo, hi), ConstLit::Int(n)) => lo.value <= *n && *n <= hi.value,
233 (PredKind::NonNegative, ConstLit::Float(v)) => *v >= 0.0,
234 (PredKind::Positive, ConstLit::Float(v)) => *v > 0.0,
235 (PredKind::InRangeF(lo, hi), ConstLit::Float(v)) => lo.value <= *v && *v <= hi.value,
236 (PredKind::MinLength(k), ConstLit::Str(s)) => s.chars().count() as i64 >= *k,
237 (PredKind::MaxLength(k), ConstLit::Str(s)) => (s.chars().count() as i64) <= *k,
238 (PredKind::Length(k), ConstLit::Str(s)) => s.chars().count() as i64 == *k,
239 (PredKind::NonEmpty, ConstLit::Str(s)) => !s.is_empty(),
240 (PredKind::Matches(pat), ConstLit::Str(s)) => {
241 regress::Regex::new(&format!("^(?:{pat})$"))
244 .map(|re| re.find(s).is_some())
245 .unwrap_or(false)
246 }
247 _ => true,
248 }
249}
250
251pub(crate) fn first_failed_predicate<'a>(
253 refinement: &'a Refinement,
254 lit: &ConstLit,
255) -> Option<&'a PredKind> {
256 bynk_syntax::ast::in_check_order(&refinement.predicates, |p| &p.kind)
259 .into_iter()
260 .find(|p| !eval_predicate(&p.kind, lit))
261 .map(|p| &p.kind)
262}
263
264fn locale_tag_refinement() -> Option<&'static Refinement> {
270 use std::sync::OnceLock;
271 static REFINEMENT: OnceLock<Option<Refinement>> = OnceLock::new();
272 REFINEMENT
273 .get_or_init(|| {
274 let src = crate::firstparty::BYNK_LOCALE_TYPES_SRC;
275 let tokens = bynk_syntax::lexer::tokenize(src).ok()?;
276 let unit = bynk_syntax::parser::parse_unit(&tokens, src).ok()?;
277 let bynk_syntax::ast::SourceUnit::Commons(commons) = unit else {
278 return None;
279 };
280 commons.items.iter().find_map(|item| match item {
281 CommonsItem::Type(t) if t.name.name == "LocaleTag" => {
282 type_decl_refinement(t).cloned()
283 }
284 _ => None,
285 })
286 })
287 .as_ref()
288}
289
290pub fn locale_tag_pattern() -> Option<&'static str> {
293 locale_tag_refinement()?.predicates.iter().find_map(|p| {
294 if let PredKind::Matches(pat) = &p.kind {
295 Some(pat.as_str())
296 } else {
297 None
298 }
299 })
300}
301
302pub fn locale_tag_accepts(tag: &str) -> bool {
307 match locale_tag_refinement() {
308 Some(refinement) => {
309 first_failed_predicate(refinement, &ConstLit::Str(tag.to_string())).is_none()
310 }
311 None => true,
312 }
313}
314
315pub(crate) fn literal_matches_base(lit: &ConstLit, base: BaseType) -> bool {
316 matches!(
317 (lit, base),
318 (ConstLit::Int(_), BaseType::Int)
319 | (ConstLit::Str(_), BaseType::String)
320 | (ConstLit::Bool(_), BaseType::Bool)
321 | (ConstLit::Float(_), BaseType::Float)
322 )
323}
324
325pub(crate) fn admit_refined_literal(
343 expr: &Expr,
344 expected: Option<TyId>,
345 ctx: &mut Ctx,
346) -> Option<TyId> {
347 let tys = ctx.tys;
348 let expected = match expected.map(|e| tys.get(e)).as_deref() {
349 Some(Ty::Effect(inner)) => Some(*inner),
350 _ => expected,
351 };
352 let expected_node = expected.map(|e| tys.get(e));
353 let Some(Ty::Named {
354 name,
355 kind: NamedKind::Refined(base),
356 ..
357 }) = expected_node.as_deref()
358 else {
359 return None;
360 };
361 let lit = const_literal(expr)?;
362 if !literal_matches_base(&lit, *base) {
363 return None;
364 }
365 let decl = ctx.input.types.get(name)?.clone();
366 if let Some(refinement) = type_decl_refinement(&decl)
367 && let Some(failed) = first_failed_predicate(refinement, &lit)
368 {
369 ctx.errors.push(CompileError::new(
370 "bynk.refine.literal_violates",
371 expr.span,
372 format!(
373 "literal {} does not satisfy `{}` required by type `{}`",
374 lit.display(),
375 failed.name(),
376 name
377 ),
378 ));
379 }
380 Some(named_ty(&decl, tys))
381}
382
383pub(crate) fn check_refinement(
384 base: BaseType,
385 base_span: Span,
386 refinement: Option<&Refinement>,
387 errors: &mut Vec<CompileError>,
388) {
389 let Some(refinement) = refinement else {
390 return;
391 };
392
393 for pred in &refinement.predicates {
394 if !pred_applies_to(&pred.kind, base) {
395 let numeric_bound_mismatch = matches!(
399 (&pred.kind, base),
400 (PredKind::InRange(_, _), BaseType::Float)
401 | (PredKind::InRangeF(_, _), BaseType::Int)
402 );
403 if numeric_bound_mismatch {
404 let (bounds, want) = if base == BaseType::Float {
405 ("`Int`", "`InRange(0.0, 1.0)`")
406 } else {
407 ("`Float`", "`InRange(0, 1)`")
408 };
409 errors.push(
410 CompileError::new(
411 "bynk.types.no_numeric_coercion",
412 pred.span,
413 format!(
414 "`InRange` bounds are {bounds} literals, but the base type is `{}`",
415 base.name()
416 ),
417 )
418 .with_label(
419 base_span,
420 format!("base type `{}` declared here", base.name()),
421 )
422 .with_note(format!(
423 "refinement bounds must match the base type — e.g. {want}"
424 )),
425 );
426 continue;
427 }
428 errors.push(
429 CompileError::new(
430 "bynk.types.predicate_base_mismatch",
431 pred.span,
432 format!(
433 "predicate `{}` cannot be applied to base type `{}`",
434 pred.kind.name(),
435 base.name()
436 ),
437 )
438 .with_label(
439 base_span,
440 format!("base type `{}` declared here", base.name()),
441 )
442 .with_note(predicate_base_help(pred.kind.name())),
443 );
444 }
445 match &pred.kind {
446 PredKind::Matches(pat) => {
447 if let Err(e) = regress::Regex::new(pat) {
454 errors.push(
455 CompileError::new(
456 "bynk.types.invalid_regex",
457 pred.span,
458 format!("invalid regular expression in `Matches(\"{pat}\")`"),
459 )
460 .with_note(format!("regex parse error (JS `RegExp` semantics): {e}")),
461 );
462 } else if has_nested_unbounded_quantifier(pat) {
463 errors.push(
473 CompileError::new(
474 "bynk.types.catastrophic_regex",
475 pred.span,
476 format!(
477 "the pattern in `Matches(\"{pat}\")` nests unbounded quantifiers, \
478 which can cause catastrophic backtracking (ReDoS)"
479 ),
480 )
481 .with_note(
482 "a repeated group that itself contains `*`, `+`, or `{n,}` makes \
483 matching take exponential time on crafted input; restructure the \
484 pattern so no unbounded quantifier is nested inside another",
485 ),
486 );
487 } else if let Some(e) = ambiguity_diagnostic(pat, pred.span, refinement) {
488 errors.push(e);
489 }
490 }
491 PredKind::InRange(lo, hi) => {
492 if lo.value > hi.value {
493 errors.push(
494 CompileError::new(
495 "bynk.types.inverted_range",
496 pred.span,
497 format!(
498 "`InRange({}, {})` has its bounds inverted (`min` must be ≤ `max`)",
499 lo.value, hi.value
500 ),
501 )
502 .with_suggestion(
505 "swap the bounds",
506 vec![
507 (lo.span, hi.value.to_string()),
508 (hi.span, lo.value.to_string()),
509 ],
510 Applicability::MachineApplicable,
511 ),
512 );
513 }
514 }
515 PredKind::InRangeF(lo, hi) => {
516 if lo.value > hi.value {
517 errors.push(
518 CompileError::new(
519 "bynk.types.inverted_range",
520 pred.span,
521 format!(
522 "`InRange({}, {})` has its bounds inverted (`min` must be ≤ `max`)",
523 lo.lexeme, hi.lexeme
524 ),
525 )
526 .with_suggestion(
527 "swap the bounds",
528 vec![(lo.span, hi.lexeme.clone()), (hi.span, lo.lexeme.clone())],
529 Applicability::MachineApplicable,
530 ),
531 );
532 }
533 }
534 PredKind::MinLength(n) | PredKind::MaxLength(n) | PredKind::Length(n) => {
535 if *n < 0 {
536 errors.push(CompileError::new(
537 "bynk.types.negative_length",
538 pred.span,
539 format!("length argument must be non-negative, got {n}"),
540 ));
541 }
542 }
543 PredKind::NonNegative | PredKind::Positive | PredKind::NonEmpty => {}
544 }
545 }
546
547 let all_compatible = refinement
548 .predicates
549 .iter()
550 .all(|p| pred_applies_to(&p.kind, base));
551 if !all_compatible {
552 return;
553 }
554 match base {
555 BaseType::Int => check_int_refinement_consistency(refinement, errors),
556 BaseType::String => check_string_refinement_consistency(refinement, errors),
557 BaseType::Bool => {}
558 BaseType::Float => check_float_refinement_consistency(refinement, errors),
559 BaseType::Duration | BaseType::Instant | BaseType::Bytes => {}
564 }
565}
566
567fn pred_applies_to(pred: &PredKind, base: BaseType) -> bool {
568 matches!(
569 (pred, base),
570 (PredKind::Matches(_), BaseType::String)
571 | (PredKind::InRange(_, _), BaseType::Int)
572 | (PredKind::InRangeF(_, _), BaseType::Float)
573 | (PredKind::MinLength(_), BaseType::String)
574 | (PredKind::MaxLength(_), BaseType::String)
575 | (PredKind::Length(_), BaseType::String)
576 | (PredKind::NonNegative, BaseType::Int | BaseType::Float)
577 | (PredKind::Positive, BaseType::Int | BaseType::Float)
578 | (PredKind::NonEmpty, BaseType::String)
579 )
580}
581
582fn predicate_base_help(name: &str) -> &'static str {
583 match name {
584 "Matches" | "MinLength" | "MaxLength" | "Length" | "NonEmpty" => {
585 "this predicate applies to `String` only"
586 }
587 "NonNegative" | "Positive" => "this predicate applies to `Int` and `Float` only",
588 "InRange" => {
589 "this predicate applies to `Int` and `Float` only, with bounds matching the base"
590 }
591 _ => "see the documentation for valid predicate-base combinations",
592 }
593}
594
595pub(crate) fn check_int_refinement_consistency(
596 refinement: &Refinement,
597 errors: &mut Vec<CompileError>,
598) {
599 let mut lo: i64 = i64::MIN;
600 let mut hi: i64 = i64::MAX;
601 for p in &refinement.predicates {
602 match &p.kind {
603 PredKind::Positive => lo = lo.max(1),
604 PredKind::NonNegative => lo = lo.max(0),
605 PredKind::InRange(a, b) => {
606 lo = lo.max(a.value);
607 hi = hi.min(b.value);
608 }
609 _ => {}
610 }
611 }
612 if lo > hi {
613 errors.push(
614 CompileError::new(
615 "bynk.types.empty_refinement",
616 refinement.span,
617 "this refinement has no valid values — the predicates contradict each other",
618 )
619 .with_note(format!(
620 "the effective range is `{lo}..={hi}`, which is empty"
621 )),
622 );
623 }
624}
625
626pub(crate) fn check_float_refinement_consistency(
627 refinement: &Refinement,
628 errors: &mut Vec<CompileError>,
629) {
630 let mut lo = f64::NEG_INFINITY;
631 let mut hi = f64::INFINITY;
632 let mut lo_exclusive = false;
634 for p in &refinement.predicates {
635 match &p.kind {
636 PredKind::Positive if 0.0 >= lo => {
637 lo = 0.0;
638 lo_exclusive = true;
639 }
640 PredKind::NonNegative if 0.0 > lo => {
641 lo = 0.0;
642 lo_exclusive = false;
643 }
644 PredKind::InRangeF(a, b) => {
645 if a.value > lo {
646 lo = a.value;
647 lo_exclusive = false;
648 }
649 hi = hi.min(b.value);
650 }
651 _ => {}
652 }
653 }
654 if lo > hi || (lo == hi && lo_exclusive) {
655 errors.push(
656 CompileError::new(
657 "bynk.types.empty_refinement",
658 refinement.span,
659 "this refinement has no valid values — the predicates contradict each other",
660 )
661 .with_note(format!(
662 "the effective range is `{lo}..={hi}`{}, which is empty",
663 if lo_exclusive {
664 " (lower bound exclusive)"
665 } else {
666 ""
667 }
668 )),
669 );
670 }
671}
672
673pub(crate) fn check_string_refinement_consistency(
674 refinement: &Refinement,
675 errors: &mut Vec<CompileError>,
676) {
677 let mut min_len: i64 = 0;
678 let mut max_len: i64 = i64::MAX;
679 let mut exact_len: Option<i64> = None;
680 for p in &refinement.predicates {
681 match &p.kind {
682 PredKind::MinLength(n) => min_len = min_len.max(*n),
683 PredKind::MaxLength(n) => max_len = max_len.min(*n),
684 PredKind::NonEmpty => min_len = min_len.max(1),
685 PredKind::Length(n) => {
686 if let Some(prev) = exact_len {
687 if prev != *n {
688 errors.push(CompileError::new(
689 "bynk.types.empty_refinement",
690 refinement.span,
691 format!(
692 "conflicting exact lengths: `Length({prev})` and `Length({n})` cannot both hold"
693 ),
694 ));
695 }
696 } else {
697 exact_len = Some(*n);
698 }
699 min_len = min_len.max(*n);
700 max_len = max_len.min(*n);
701 }
702 _ => {}
703 }
704 }
705 if min_len > max_len {
706 errors.push(
707 CompileError::new(
708 "bynk.types.empty_refinement",
709 refinement.span,
710 "this refinement has no valid values — minimum length exceeds maximum length",
711 )
712 .with_note(format!(
713 "the effective length range is `{min_len}..={max_len}`, which is empty"
714 )),
715 );
716 }
717}
718
719pub(crate) fn refinement_needs_pin(refinement: &Refinement) -> bool {
727 refinement
728 .predicates
729 .iter()
730 .any(|p| matches!(p.kind, PredKind::Matches(_)))
731}
732
733pub fn zero_value_ts(
736 type_ref: &TypeRef,
737 inline: Option<&Refinement>,
738 types: &HashMap<String, Arc<TypeDecl>>,
739) -> Option<String> {
740 zero_value_ts_inner(type_ref, inline, types, &mut Vec::new())
741}
742
743fn zero_value_ts_inner(
744 type_ref: &TypeRef,
745 inline: Option<&Refinement>,
746 types: &HashMap<String, Arc<TypeDecl>>,
747 visiting: &mut Vec<String>,
748) -> Option<String> {
749 match type_ref {
750 TypeRef::Base(b, _) => {
751 if refinement_admits_zero(*b, inline) {
752 zero_of_base(*b)
753 } else {
754 None
755 }
756 }
757 TypeRef::Option(_, _) => Some("None".to_string()),
759 TypeRef::Named(id) => {
760 let decl = types.get(&id.name)?;
761 match &decl.body {
762 TypeBody::Refined {
763 base, refinement, ..
764 } => {
765 if refinement_admits_zero(*base, refinement.as_ref()) {
766 zero_of_base(*base)
767 } else {
768 None
769 }
770 }
771 TypeBody::Record(rec) => {
772 if visiting.iter().any(|n| n == &id.name) {
778 return None;
779 }
780 visiting.push(id.name.clone());
781 let z = agent_state_zero_record(&rec.fields, types, visiting);
782 visiting.pop();
783 z
784 }
785 TypeBody::Sum(_) | TypeBody::Opaque { .. } => None,
787 }
788 }
789 _ => None,
792 }
793}
794
795fn agent_state_zero_record(
798 fields: &[RecordField],
799 types: &HashMap<String, Arc<TypeDecl>>,
800 visiting: &mut Vec<String>,
801) -> Option<String> {
802 let mut parts = Vec::new();
803 for f in fields {
804 let z = zero_value_ts_inner(&f.type_ref, f.refinement.as_ref(), types, visiting)?;
805 parts.push(format!("{}: {}", f.name.name, z));
806 }
807 Some(format!("{{ {} }}", parts.join(", ")))
808}
809
810fn zero_of_base(b: BaseType) -> Option<String> {
811 Some(
812 match b {
813 BaseType::Int => "0",
814 BaseType::Bool => "false",
815 BaseType::String => "\"\"",
816 BaseType::Float => "0",
817 BaseType::Duration | BaseType::Instant => "0",
820 BaseType::Bytes => "new globalThis.Uint8Array()",
823 }
824 .to_string(),
825 )
826}
827
828fn refinement_admits_zero(base: BaseType, refinement: Option<&Refinement>) -> bool {
833 let Some(r) = refinement else {
834 return true;
835 };
836 r.predicates.iter().all(|p| pred_admits_zero(base, &p.kind))
837}
838
839fn pred_admits_zero(base: BaseType, k: &PredKind) -> bool {
840 match base {
841 BaseType::Int => match k {
842 PredKind::NonNegative => true,
843 PredKind::Positive => false,
844 PredKind::InRange(lo, hi) => lo.value <= 0 && 0 <= hi.value,
845 _ => false,
847 },
848 BaseType::String => match k {
849 PredKind::Matches(p) => regex_matches_empty(p),
850 PredKind::MinLength(n) => *n <= 0,
851 PredKind::MaxLength(n) => *n >= 0,
852 PredKind::Length(n) => *n == 0,
853 PredKind::NonEmpty => false,
854 _ => false,
856 },
857 BaseType::Bool => true,
859 BaseType::Duration | BaseType::Instant | BaseType::Bytes => true,
862 BaseType::Float => match k {
863 PredKind::NonNegative => true,
864 PredKind::Positive => false,
865 PredKind::InRangeF(lo, hi) => lo.value <= 0.0 && 0.0 <= hi.value,
866 _ => false,
868 },
869 }
870}
871
872fn regex_matches_empty(pattern: &str) -> bool {
876 match regress::Regex::new(&format!("^(?:{pattern})$")) {
877 Ok(re) => re.find("").is_some(),
878 Err(_) => false,
879 }
880}
881
882fn ambiguity_diagnostic(pat: &str, span: Span, refinement: &Refinement) -> Option<CompileError> {
893 use super::regex_ambiguity::{Verdict, analyse};
894 Some(match analyse(pat) {
895 Verdict::Linear => return None,
896 Verdict::Exponential(a, b) => CompileError::new(
897 "bynk.types.catastrophic_regex",
898 span,
899 format!(
900 "the pattern in `Matches(\"{pat}\")` can match the same text in \
901 exponentially many ways, which can cause catastrophic backtracking (ReDoS)"
902 ),
903 )
904 .with_note(format!(
905 "inside a repetition, `{a}` and `{b}` can match the same character on \
906 different paths, so each repetition doubles the ways a near-miss input \
907 is tried; make the alternatives (or repeat counts) inside the loop \
908 unambiguous, e.g. `(a|aa)+` as `a+`"
909 )),
910 Verdict::Unanalysable(why) => CompileError::new(
911 "bynk.types.catastrophic_regex",
912 span,
913 format!(
914 "the pattern in `Matches(\"{pat}\")` cannot be shown free of catastrophic \
915 backtracking (ReDoS): {why}"
916 ),
917 )
918 .with_note(
919 "the boundary check runs this pattern on request input under a backtracking \
920 engine, so a pattern that cannot be analysed is rejected; simplify it",
921 ),
922 Verdict::Polynomial(degree, a, b) => {
923 let bound = refinement
927 .predicates
928 .iter()
929 .filter_map(|p| match p.kind {
930 PredKind::MaxLength(n) | PredKind::Length(n) => Some(n),
931 _ => None,
932 })
933 .min();
934 let largest = largest_safe_bound(degree);
935 let (code, advice) = match bound {
936 Some(n) if n <= largest => (
937 "bynk.types.polynomial_regex_capped",
938 format!(
939 "the refinement bounds the input at {n} characters, which keeps \
940 the cost small, so this is a warning; rewrite the pattern so the \
941 repetitions cannot overlap to silence it"
942 ),
943 ),
944 Some(n) => (
945 "bynk.types.polynomial_regex",
946 format!(
947 "the refinement bounds the input at {n} characters, but at degree \
948 {degree} that still allows about {n}^{degree} steps; lower the \
949 bound to at most {largest}, or rewrite the pattern so the \
950 repetitions cannot overlap"
951 ),
952 ),
953 None => (
954 "bynk.types.polynomial_regex",
955 format!(
956 "add a `MaxLength(n)` predicate with `n` at most {largest} to bound \
957 the input, or rewrite the pattern so the repetitions cannot overlap"
958 ),
959 ),
960 };
961 CompileError::new(
962 code,
963 span,
964 format!(
965 "the pattern in `Matches(\"{pat}\")` can split the same text between \
966 repetitions in many ways, so matching time grows with the input's \
967 length to the power {degree} (ReDoS)"
968 ),
969 )
970 .with_note(format!(
971 "`{a}` and `{b}` repeat in sequence and can match the same characters, as \
972 in `\\d*\\d*`; {advice}"
973 ))
974 }
975 })
976}
977
978const POLYNOMIAL_STEP_CAP: u128 = 10_000_000;
982
983fn largest_safe_bound(degree: u32) -> i64 {
986 let mut n: i64 = 1;
987 while (n as u128 + 1)
988 .checked_pow(degree)
989 .is_some_and(|s| s <= POLYNOMIAL_STEP_CAP)
990 {
991 n += 1;
992 }
993 n
994}
995
996fn has_nested_unbounded_quantifier(pat: &str) -> bool {
1020 let chars: Vec<char> = pat.chars().collect();
1026 let mut stack: Vec<bool> = vec![false];
1029 let mut pending: Option<bool> = None;
1033 let mut i = 0;
1034
1035 fn fold(pending: &mut Option<bool>, stack: &mut [bool]) {
1039 if pending.take() == Some(true) {
1040 *stack.last_mut().unwrap() = true;
1041 }
1042 }
1043
1044 while i < chars.len() {
1045 match chars[i] {
1046 '\\' => {
1048 fold(&mut pending, &mut stack);
1049 i += 2;
1050 pending = Some(false);
1051 }
1052 '[' => {
1054 fold(&mut pending, &mut stack);
1055 i += 1;
1056 if i < chars.len() && chars[i] == '^' {
1057 i += 1;
1058 }
1059 if i < chars.len() && chars[i] == ']' {
1061 i += 1;
1062 }
1063 while i < chars.len() && chars[i] != ']' {
1064 if chars[i] == '\\' {
1065 i += 1;
1066 }
1067 i += 1;
1068 }
1069 i += 1; pending = Some(false);
1071 }
1072 '(' => {
1073 fold(&mut pending, &mut stack);
1074 stack.push(false);
1075 i += 1;
1076 if i < chars.len() && chars[i] == '?' {
1079 i += 1;
1080 if i < chars.len() && matches!(chars[i], ':' | '=' | '!') {
1081 i += 1;
1082 } else if i < chars.len() && chars[i] == '<' {
1083 i += 1;
1084 if i < chars.len() && matches!(chars[i], '=' | '!') {
1085 i += 1;
1086 } else {
1087 while i < chars.len() && chars[i] != '>' {
1088 i += 1;
1089 }
1090 if i < chars.len() {
1091 i += 1; }
1093 }
1094 }
1095 }
1096 }
1097 ')' => {
1098 fold(&mut pending, &mut stack);
1099 let closed = stack.pop().unwrap_or(false);
1100 i += 1;
1101 pending = Some(closed);
1103 }
1104 '|' => {
1106 fold(&mut pending, &mut stack);
1107 i += 1;
1108 }
1109 '*' | '+' => {
1111 let atom_unbounded = pending.take().unwrap_or(false);
1112 if atom_unbounded {
1113 return true; }
1115 *stack.last_mut().unwrap() = true;
1116 i += 1;
1117 if i < chars.len() && chars[i] == '?' {
1118 i += 1; }
1120 }
1121 '?' => {
1123 if let Some(atom_unbounded) = pending.take() {
1124 if atom_unbounded {
1125 *stack.last_mut().unwrap() = true;
1126 }
1127 i += 1;
1128 if i < chars.len() && chars[i] == '?' {
1129 i += 1; }
1131 } else {
1132 i += 1;
1134 pending = Some(false);
1135 }
1136 }
1137 '{' => {
1138 if let Some((unbounded_q, next)) = parse_brace_quantifier(&chars, i) {
1139 let atom_unbounded = pending.take().unwrap_or(false);
1140 if unbounded_q && atom_unbounded {
1141 return true;
1142 }
1143 if unbounded_q || atom_unbounded {
1144 *stack.last_mut().unwrap() = true;
1145 }
1146 i = next;
1147 if i < chars.len() && chars[i] == '?' {
1148 i += 1; }
1150 } else {
1151 fold(&mut pending, &mut stack);
1153 i += 1;
1154 pending = Some(false);
1155 }
1156 }
1157 _ => {
1159 fold(&mut pending, &mut stack);
1160 i += 1;
1161 pending = Some(false);
1162 }
1163 }
1164 }
1165 false
1166}
1167
1168fn parse_brace_quantifier(chars: &[char], start: usize) -> Option<(bool, usize)> {
1174 let mut i = start + 1;
1175 let lo_start = i;
1176 while i < chars.len() && chars[i].is_ascii_digit() {
1177 i += 1;
1178 }
1179 if i == lo_start {
1180 return None; }
1182 let mut unbounded = false;
1183 if i < chars.len() && chars[i] == ',' {
1184 i += 1;
1185 let hi_start = i;
1186 while i < chars.len() && chars[i].is_ascii_digit() {
1187 i += 1;
1188 }
1189 if i == hi_start {
1190 unbounded = true; }
1192 }
1193 if i < chars.len() && chars[i] == '}' {
1194 Some((unbounded, i + 1))
1195 } else {
1196 None
1197 }
1198}
1199
1200#[cfg(test)]
1201mod redos_tests {
1202 use super::has_nested_unbounded_quantifier as redos;
1203
1204 #[test]
1205 fn flags_nested_unbounded_quantifiers() {
1206 assert!(redos("(a+)+"));
1209 assert!(redos("(a+)+$"));
1210 assert!(redos("(a*)*"));
1211 assert!(redos("(a+)*"));
1212 assert!(redos("(a*)+"));
1213 assert!(redos("((ab)+)+"));
1214 assert!(redos("(a{1,})+")); assert!(redos("(a+){2,}")); assert!(redos("x(y(z+)+w)+")); assert!(redos("(\\d+)+"));
1218 assert!(redos("(?:a+)+")); assert!(redos("([a-z]+)*")); assert!(redos("((a+)?)+")); assert!(redos("(a+|b)+")); }
1223
1224 #[test]
1225 fn allows_safe_patterns() {
1226 assert!(!redos("a+"));
1228 assert!(!redos("(a+)")); assert!(!redos("(a+)(b+)")); assert!(!redos("(ab)+")); assert!(!redos("(a+)?")); assert!(!redos("(a+){2,3}")); assert!(!redos("(a{2,3})+"));
1236 assert!(!redos("[a-z]+")); assert!(!redos("a{2,}b{2,}")); }
1239
1240 #[test]
1241 fn the_ambiguity_analysis_covers_what_the_first_pass_does_not() {
1242 use super::super::regex_ambiguity::{Verdict, analyse};
1246 for pat in [
1247 "(a|a)+",
1248 "(\\d|\\d\\d)+",
1249 "(a|aa)*",
1250 "(a{1,2})+",
1251 "(a{2,3})+",
1252 ] {
1253 assert!(!redos(pat), "the first pass is not meant to flag `{pat}`");
1254 assert!(
1255 matches!(analyse(pat), Verdict::Exponential(..)),
1256 "`{pat}` is exponential"
1257 );
1258 }
1259 assert!(matches!(analyse("\\d*\\d*"), Verdict::Polynomial(..)));
1260 assert_eq!(analyse("(foo|foobar)+"), Verdict::Linear);
1261 }
1262
1263 #[test]
1264 fn allows_every_pattern_used_in_the_repo() {
1265 for pat in [
1268 "[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}",
1269 "[a-z][a-z0-9-]*",
1270 "[A-Z]{3}-[0-9]{4}",
1271 "[A-Z]{3}",
1272 "[a-z]+",
1273 "[A-Z]+",
1274 "[a-z]+(?<=ing)",
1275 "[a-z0-9_]+",
1276 "[a-z0-9]{1,16}",
1277 "[a-z0-9]{1,8}",
1278 "[A-Z0-9]{3,16}",
1279 "[a-z0-9]{3,8}",
1280 "[a-zA-Z0-9]{6,8}",
1281 "ab|cd",
1282 "AUTH-[0-9]{8}",
1283 "AUTH-[0-9]+",
1284 "CUST-[0-9]+",
1285 "https?://.+",
1286 "ORD-[0-9]{6}",
1287 "ORD-[0-9]+",
1288 "SHP-[0-9]{8}",
1289 "T-[0-9]+",
1290 "[A-Z0-9]{8}",
1291 "[A-Z]{2}[0-9]{2}",
1292 "^[a-z]+$",
1293 "\\d{4}",
1294 "\\d+",
1295 ] {
1296 assert!(!redos(pat), "false positive on safe pattern `{pat}`");
1297 assert_eq!(
1298 super::super::regex_ambiguity::analyse(pat),
1299 super::super::regex_ambiguity::Verdict::Linear,
1300 "the ambiguity analysis rejects safe pattern `{pat}`"
1301 );
1302 }
1303 let locale_pat = super::locale_tag_pattern().expect("LocaleTag has a Matches predicate");
1307 assert!(
1308 !redos(locale_pat),
1309 "false positive on safe pattern `{locale_pat}`"
1310 );
1311 assert_eq!(
1312 super::super::regex_ambiguity::analyse(locale_pat),
1313 super::super::regex_ambiguity::Verdict::Linear,
1314 "the ambiguity analysis rejects `LocaleTag`"
1315 );
1316 }
1317}
1318
1319#[cfg(test)]
1320mod locale_tag_tests {
1321 use super::locale_tag_accepts;
1322
1323 #[test]
1324 fn admits_the_pre_existing_shapes() {
1325 for tag in ["en", "pt-BR", "zh-Hans-CN", "es-419"] {
1326 assert!(locale_tag_accepts(tag), "expected `{tag}` to be admitted");
1327 }
1328 }
1329
1330 #[test]
1331 fn admits_variants_extensions_private_use_and_extlang() {
1332 for tag in [
1333 "de-CH-1996", "ca-valencia", "sl-rozaj", "en-scotland-fonipa", "en-US-u-ca-buddhist", "de-CH-x-phonebk", "x-custom", "zh-yue", ] {
1342 assert!(locale_tag_accepts(tag), "expected `{tag}` to be admitted");
1343 }
1344 }
1345
1346 #[test]
1347 fn rejects_grandfathered_and_malformed_tags() {
1348 for tag in [
1349 "i-klingon", "en-GB-oed", "Klingon", "pt-br", "en-SCOTLAND", "en-x-abcdefghi", "x", "",
1357 ] {
1358 assert!(!locale_tag_accepts(tag), "expected `{tag}` to be rejected");
1359 }
1360 }
1361}