1use super::*;
7
8pub(crate) fn check_type_decl(
9 t: &TypeDecl,
10 types: &HashMap<String, Arc<TypeDecl>>,
11 tys: &Types,
12 allows_field_defaults: bool,
16 errors: &mut Vec<CompileError>,
17) {
18 match &t.body {
19 TypeBody::Refined {
20 base,
21 base_span,
22 refinement,
23 } => {
24 check_refinement(*base, *base_span, refinement.as_ref(), errors);
25 }
26 TypeBody::Opaque {
27 base,
28 base_span,
29 refinement,
30 } => {
31 check_refinement(*base, *base_span, refinement.as_ref(), errors);
33 }
34 TypeBody::Record(r) => {
35 for f in &r.fields {
36 if let Some(ref_r) = &f.refinement {
37 if let Some(b) = field_base_type(&f.type_ref, types) {
39 check_refinement(b, f.type_ref.span(), Some(ref_r), errors);
40 } else {
41 errors.push(CompileError::new(
42 "bynk.types.field_refinement_not_base",
43 ref_r.span,
44 format!(
45 "inline refinement on field `{}` requires a base or refined type",
46 f.name.name
47 ),
48 ));
49 }
50 }
51 if let Some(init) = f.init.as_ref().filter(|_| !allows_field_defaults) {
62 errors.push(
63 CompileError::new(
64 "bynk.event.default_outside_event",
65 init.span,
66 format!(
67 "a field default is only meaningful on an `event`'s field, not `{}`",
68 f.name.name
69 ),
70 )
71 .with_note(
72 "a default exists so an older wire event missing this key can still \
73 deserialise — an ordinary record has no wire-evolution story, so a \
74 default here would be silently ignored",
75 ),
76 );
77 }
78 }
79 }
80 TypeBody::Sum(s) => {
81 check_embeds(&t.name.name, s, types, errors, tys);
82 }
83 }
84}
85
86pub(crate) fn check_embeds(
92 sum_name: &str,
93 s: &SumBody,
94 types: &HashMap<String, Arc<TypeDecl>>,
95 errors: &mut Vec<CompileError>,
96 tys: &Types,
97) {
98 let mut seen_sources: Vec<TyId> = Vec::new();
99 for clause in &s.embeds {
100 let Some(source_ty) = resolve_type_ref(&clause.source_type, types, tys) else {
101 continue;
104 };
105 let Some(variant) = s
107 .variants
108 .iter()
109 .find(|v| v.name.name == clause.variant.name)
110 else {
111 errors.push(CompileError::new(
112 "bynk.types.embeds_unknown_variant",
113 clause.variant.span,
114 format!(
115 "`embeds … as {}` names no variant of `{}`",
116 clause.variant.name, sum_name
117 ),
118 ));
119 continue;
120 };
121 if variant.payload.len() != 1 {
123 errors.push(
124 CompileError::new(
125 "bynk.types.embeds_variant_shape",
126 clause.span,
127 format!(
128 "`embeds … as {}` requires `{}` to have exactly one payload field, but it has {}",
129 clause.variant.name,
130 clause.variant.name,
131 variant.payload.len()
132 ),
133 )
134 .with_note("a value of the embedded type is wrapped into that single field"),
135 );
136 continue;
137 }
138 let field_ty = resolve_type_ref(&variant.payload[0].type_ref, types, tys);
139 if let Some(field_ty) = &field_ty
140 && !compatible(source_ty, *field_ty, tys)
141 {
142 errors.push(CompileError::new(
143 "bynk.types.embeds_variant_shape",
144 clause.span,
145 format!(
146 "`embeds {} as {}` — but `{}`'s payload field has type `{}`, not `{}`",
147 source_ty.display(tys),
148 clause.variant.name,
149 clause.variant.name,
150 field_ty.display(tys),
151 source_ty.display(tys)
152 ),
153 ));
154 continue;
155 }
156 if seen_sources.iter().any(|t| compatible(*t, source_ty, tys)) {
158 errors.push(CompileError::new(
159 "bynk.types.embeds_ambiguous",
160 clause.span,
161 format!(
162 "`{}` is embedded more than once by `{}` — the conversion would be ambiguous",
163 source_ty.display(tys),
164 sum_name
165 ),
166 ));
167 continue;
168 }
169 seen_sources.push(source_ty);
170 }
171}
172
173fn field_base_type(r: &TypeRef, types: &HashMap<String, Arc<TypeDecl>>) -> Option<BaseType> {
175 match r {
176 TypeRef::Base(b, _) => Some(*b),
177 TypeRef::Named(id) => match types.get(&id.name).map(|t| &t.body) {
178 Some(TypeBody::Refined { base, .. }) => Some(*base),
179 _ => None,
180 },
181 _ => None,
182 }
183}
184
185pub(crate) fn type_decl_base(decl: &TypeDecl) -> Option<BaseType> {
189 match &decl.body {
190 TypeBody::Refined { base, .. } => Some(*base),
191 TypeBody::Opaque { base, .. } => Some(*base),
192 _ => None,
193 }
194}
195
196pub(crate) fn type_decl_refinement(decl: &TypeDecl) -> Option<&Refinement> {
198 match &decl.body {
199 TypeBody::Refined { refinement, .. } | TypeBody::Opaque { refinement, .. } => {
200 refinement.as_ref()
201 }
202 _ => None,
203 }
204}
205
206pub(crate) fn const_literal(e: &Expr) -> Option<ConstLit> {
211 match &e.kind {
212 ExprKind::IntLit { value: n, .. } => Some(ConstLit::Int(*n)),
213 ExprKind::FloatLit { value, .. } => Some(ConstLit::Float(*value)),
214 ExprKind::StrLit(s) => Some(ConstLit::Str(s.clone())),
215 ExprKind::BoolLit(b) => Some(ConstLit::Bool(*b)),
216 ExprKind::UnitLit => Some(ConstLit::Unit),
217 ExprKind::UnaryOp(UnaryOp::Neg, inner) => match &inner.kind {
218 ExprKind::IntLit { value: n, .. } => Some(ConstLit::Int(n.checked_neg()?)),
219 ExprKind::FloatLit { value, .. } => Some(ConstLit::Float(-*value)),
220 _ => None,
221 },
222 _ => None,
223 }
224}
225
226pub(crate) fn eval_predicate(pred: &PredKind, lit: &ConstLit) -> bool {
233 match (pred, lit) {
234 (PredKind::NonNegative, ConstLit::Int(n)) => *n >= 0,
235 (PredKind::Positive, ConstLit::Int(n)) => *n > 0,
236 (PredKind::InRange(lo, hi), ConstLit::Int(n)) => lo.value <= *n && *n <= hi.value,
237 (PredKind::NonNegative, ConstLit::Float(v)) => *v >= 0.0,
238 (PredKind::Positive, ConstLit::Float(v)) => *v > 0.0,
239 (PredKind::InRangeF(lo, hi), ConstLit::Float(v)) => lo.value <= *v && *v <= hi.value,
240 (PredKind::MinLength(k), ConstLit::Str(s)) => s.chars().count() as i64 >= *k,
241 (PredKind::MaxLength(k), ConstLit::Str(s)) => (s.chars().count() as i64) <= *k,
242 (PredKind::Length(k), ConstLit::Str(s)) => s.chars().count() as i64 == *k,
243 (PredKind::NonEmpty, ConstLit::Str(s)) => !s.is_empty(),
244 (PredKind::Matches(pat), ConstLit::Str(s)) => {
245 regress::Regex::new(&format!("^(?:{pat})$"))
248 .map(|re| re.find(s).is_some())
249 .unwrap_or(false)
250 }
251 _ => true,
252 }
253}
254
255pub(crate) fn first_failed_predicate<'a>(
257 refinement: &'a Refinement,
258 lit: &ConstLit,
259) -> Option<&'a PredKind> {
260 bynk_syntax::ast::in_check_order(&refinement.predicates, |p| &p.kind)
263 .into_iter()
264 .find(|p| !eval_predicate(&p.kind, lit))
265 .map(|p| &p.kind)
266}
267
268fn locale_tag_refinement() -> Option<&'static Refinement> {
274 use std::sync::OnceLock;
275 static REFINEMENT: OnceLock<Option<Refinement>> = OnceLock::new();
276 REFINEMENT
277 .get_or_init(|| {
278 let src = crate::firstparty::BYNK_LOCALE_TYPES_SRC;
279 let tokens = bynk_syntax::lexer::tokenize(src).ok()?;
280 let unit = bynk_syntax::parser::parse_unit(&tokens, src).ok()?;
281 let bynk_syntax::ast::SourceUnit::Commons(commons) = unit else {
282 return None;
283 };
284 commons.items.iter().find_map(|item| match item {
285 CommonsItem::Type(t) if t.name.name == "LocaleTag" => {
286 type_decl_refinement(t).cloned()
287 }
288 _ => None,
289 })
290 })
291 .as_ref()
292}
293
294pub fn locale_tag_pattern() -> Option<&'static str> {
297 locale_tag_refinement()?.predicates.iter().find_map(|p| {
298 if let PredKind::Matches(pat) = &p.kind {
299 Some(pat.as_str())
300 } else {
301 None
302 }
303 })
304}
305
306pub fn locale_tag_accepts(tag: &str) -> bool {
311 match locale_tag_refinement() {
312 Some(refinement) => {
313 first_failed_predicate(refinement, &ConstLit::Str(tag.to_string())).is_none()
314 }
315 None => true,
316 }
317}
318
319pub(crate) fn literal_matches_base(lit: &ConstLit, base: BaseType) -> bool {
320 matches!(
321 (lit, base),
322 (ConstLit::Int(_), BaseType::Int)
323 | (ConstLit::Str(_), BaseType::String)
324 | (ConstLit::Bool(_), BaseType::Bool)
325 | (ConstLit::Float(_), BaseType::Float)
326 )
327}
328
329pub(crate) fn admit_refined_literal(
347 expr: &Expr,
348 expected: Option<TyId>,
349 ctx: &mut Ctx,
350) -> Option<TyId> {
351 let input = ctx.input;
352 admit_refined_literal_in(expr, expected, &input.types, ctx)
353}
354
355pub(crate) fn admit_refined_literal_in(
362 expr: &Expr,
363 expected: Option<TyId>,
364 types: &HashMap<String, Arc<TypeDecl>>,
365 ctx: &mut Ctx,
366) -> Option<TyId> {
367 let tys = ctx.tys;
368 let expected = match expected.map(|e| tys.get(e)).as_deref() {
369 Some(Ty::Effect(inner)) => Some(*inner),
370 _ => expected,
371 };
372 let expected_node = expected.map(|e| tys.get(e));
373 let Some(Ty::Named {
374 name,
375 kind: NamedKind::Refined(base),
376 ..
377 }) = expected_node.as_deref()
378 else {
379 return None;
380 };
381 let lit = const_literal(expr)?;
382 if !literal_matches_base(&lit, *base) {
383 return None;
384 }
385 let decl = types.get(name)?.clone();
386 if let Some(refinement) = type_decl_refinement(&decl)
387 && let Some(failed) = first_failed_predicate(refinement, &lit)
388 {
389 ctx.errors.push(CompileError::new(
390 "bynk.refine.literal_violates",
391 expr.span,
392 format!(
393 "literal {} does not satisfy `{}` required by type `{}`",
394 lit.display(),
395 failed.name(),
396 name
397 ),
398 ));
399 }
400 Some(named_ty(&decl, tys))
401}
402
403pub(crate) fn check_refinement(
404 base: BaseType,
405 base_span: Span,
406 refinement: Option<&Refinement>,
407 errors: &mut Vec<CompileError>,
408) {
409 let Some(refinement) = refinement else {
410 return;
411 };
412
413 for pred in &refinement.predicates {
414 if !pred_applies_to(&pred.kind, base) {
415 let numeric_bound_mismatch = matches!(
419 (&pred.kind, base),
420 (PredKind::InRange(_, _), BaseType::Float)
421 | (PredKind::InRangeF(_, _), BaseType::Int)
422 );
423 if numeric_bound_mismatch {
424 let (bounds, want) = if base == BaseType::Float {
425 ("`Int`", "`InRange(0.0, 1.0)`")
426 } else {
427 ("`Float`", "`InRange(0, 1)`")
428 };
429 errors.push(
430 CompileError::new(
431 "bynk.types.no_numeric_coercion",
432 pred.span,
433 format!(
434 "`InRange` bounds are {bounds} literals, but the base type is `{}`",
435 base.name()
436 ),
437 )
438 .with_label(
439 base_span,
440 format!("base type `{}` declared here", base.name()),
441 )
442 .with_note(format!(
443 "refinement bounds must match the base type — e.g. {want}"
444 )),
445 );
446 continue;
447 }
448 errors.push(
449 CompileError::new(
450 "bynk.types.predicate_base_mismatch",
451 pred.span,
452 format!(
453 "predicate `{}` cannot be applied to base type `{}`",
454 pred.kind.name(),
455 base.name()
456 ),
457 )
458 .with_label(
459 base_span,
460 format!("base type `{}` declared here", base.name()),
461 )
462 .with_note(predicate_base_help(pred.kind.name())),
463 );
464 }
465 match &pred.kind {
466 PredKind::Matches(pat) => {
467 if let Err(e) = regress::Regex::new(pat) {
474 errors.push(
475 CompileError::new(
476 "bynk.types.invalid_regex",
477 pred.span,
478 format!("invalid regular expression in `Matches(\"{pat}\")`"),
479 )
480 .with_note(format!("regex parse error (JS `RegExp` semantics): {e}")),
481 );
482 } else if has_nested_unbounded_quantifier(pat) {
483 errors.push(
493 CompileError::new(
494 "bynk.types.catastrophic_regex",
495 pred.span,
496 format!(
497 "the pattern in `Matches(\"{pat}\")` nests unbounded quantifiers, \
498 which can cause catastrophic backtracking (ReDoS)"
499 ),
500 )
501 .with_note(
502 "a repeated group that itself contains `*`, `+`, or `{n,}` makes \
503 matching take exponential time on crafted input; restructure the \
504 pattern so no unbounded quantifier is nested inside another",
505 ),
506 );
507 } else if let Some(e) = ambiguity_diagnostic(pat, pred.span, refinement) {
508 errors.push(e);
509 }
510 }
511 PredKind::InRange(lo, hi) => {
512 if lo.value > hi.value {
513 errors.push(
514 CompileError::new(
515 "bynk.types.inverted_range",
516 pred.span,
517 format!(
518 "`InRange({}, {})` has its bounds inverted (`min` must be ≤ `max`)",
519 lo.value, hi.value
520 ),
521 )
522 .with_suggestion(
525 "swap the bounds",
526 vec![
527 (lo.span, hi.value.to_string()),
528 (hi.span, lo.value.to_string()),
529 ],
530 Applicability::MachineApplicable,
531 ),
532 );
533 }
534 }
535 PredKind::InRangeF(lo, hi) => {
536 if lo.value > hi.value {
537 errors.push(
538 CompileError::new(
539 "bynk.types.inverted_range",
540 pred.span,
541 format!(
542 "`InRange({}, {})` has its bounds inverted (`min` must be ≤ `max`)",
543 lo.lexeme, hi.lexeme
544 ),
545 )
546 .with_suggestion(
547 "swap the bounds",
548 vec![(lo.span, hi.lexeme.clone()), (hi.span, lo.lexeme.clone())],
549 Applicability::MachineApplicable,
550 ),
551 );
552 }
553 }
554 PredKind::MinLength(n) | PredKind::MaxLength(n) | PredKind::Length(n) => {
555 if *n < 0 {
556 errors.push(CompileError::new(
557 "bynk.types.negative_length",
558 pred.span,
559 format!("length argument must be non-negative, got {n}"),
560 ));
561 }
562 }
563 PredKind::NonNegative | PredKind::Positive | PredKind::NonEmpty => {}
564 }
565 }
566
567 let all_compatible = refinement
568 .predicates
569 .iter()
570 .all(|p| pred_applies_to(&p.kind, base));
571 if !all_compatible {
572 return;
573 }
574 match base {
575 BaseType::Int => check_int_refinement_consistency(refinement, errors),
576 BaseType::String => check_string_refinement_consistency(refinement, errors),
577 BaseType::Bool => {}
578 BaseType::Float => check_float_refinement_consistency(refinement, errors),
579 BaseType::Duration | BaseType::Instant | BaseType::Bytes => {}
584 }
585}
586
587fn pred_applies_to(pred: &PredKind, base: BaseType) -> bool {
588 matches!(
589 (pred, base),
590 (PredKind::Matches(_), BaseType::String)
591 | (PredKind::InRange(_, _), BaseType::Int)
592 | (PredKind::InRangeF(_, _), BaseType::Float)
593 | (PredKind::MinLength(_), BaseType::String)
594 | (PredKind::MaxLength(_), BaseType::String)
595 | (PredKind::Length(_), BaseType::String)
596 | (PredKind::NonNegative, BaseType::Int | BaseType::Float)
597 | (PredKind::Positive, BaseType::Int | BaseType::Float)
598 | (PredKind::NonEmpty, BaseType::String)
599 )
600}
601
602fn predicate_base_help(name: &str) -> &'static str {
603 match name {
604 "Matches" | "MinLength" | "MaxLength" | "Length" | "NonEmpty" => {
605 "this predicate applies to `String` only"
606 }
607 "NonNegative" | "Positive" => "this predicate applies to `Int` and `Float` only",
608 "InRange" => {
609 "this predicate applies to `Int` and `Float` only, with bounds matching the base"
610 }
611 _ => "see the documentation for valid predicate-base combinations",
612 }
613}
614
615pub(crate) fn check_int_refinement_consistency(
616 refinement: &Refinement,
617 errors: &mut Vec<CompileError>,
618) {
619 let mut lo: i64 = i64::MIN;
620 let mut hi: i64 = i64::MAX;
621 for p in &refinement.predicates {
622 match &p.kind {
623 PredKind::Positive => lo = lo.max(1),
624 PredKind::NonNegative => lo = lo.max(0),
625 PredKind::InRange(a, b) => {
626 lo = lo.max(a.value);
627 hi = hi.min(b.value);
628 }
629 _ => {}
630 }
631 }
632 if lo > hi {
633 errors.push(
634 CompileError::new(
635 "bynk.types.empty_refinement",
636 refinement.span,
637 "this refinement has no valid values — the predicates contradict each other",
638 )
639 .with_note(format!(
640 "the effective range is `{lo}..={hi}`, which is empty"
641 )),
642 );
643 }
644}
645
646pub(crate) fn check_float_refinement_consistency(
647 refinement: &Refinement,
648 errors: &mut Vec<CompileError>,
649) {
650 let mut lo = f64::NEG_INFINITY;
651 let mut hi = f64::INFINITY;
652 let mut lo_exclusive = false;
654 for p in &refinement.predicates {
655 match &p.kind {
656 PredKind::Positive if 0.0 >= lo => {
657 lo = 0.0;
658 lo_exclusive = true;
659 }
660 PredKind::NonNegative if 0.0 > lo => {
661 lo = 0.0;
662 lo_exclusive = false;
663 }
664 PredKind::InRangeF(a, b) => {
665 if a.value > lo {
666 lo = a.value;
667 lo_exclusive = false;
668 }
669 hi = hi.min(b.value);
670 }
671 _ => {}
672 }
673 }
674 if lo > hi || (lo == hi && lo_exclusive) {
675 errors.push(
676 CompileError::new(
677 "bynk.types.empty_refinement",
678 refinement.span,
679 "this refinement has no valid values — the predicates contradict each other",
680 )
681 .with_note(format!(
682 "the effective range is `{lo}..={hi}`{}, which is empty",
683 if lo_exclusive {
684 " (lower bound exclusive)"
685 } else {
686 ""
687 }
688 )),
689 );
690 }
691}
692
693pub(crate) fn check_string_refinement_consistency(
694 refinement: &Refinement,
695 errors: &mut Vec<CompileError>,
696) {
697 let mut min_len: i64 = 0;
698 let mut max_len: i64 = i64::MAX;
699 let mut exact_len: Option<i64> = None;
700 for p in &refinement.predicates {
701 match &p.kind {
702 PredKind::MinLength(n) => min_len = min_len.max(*n),
703 PredKind::MaxLength(n) => max_len = max_len.min(*n),
704 PredKind::NonEmpty => min_len = min_len.max(1),
705 PredKind::Length(n) => {
706 if let Some(prev) = exact_len {
707 if prev != *n {
708 errors.push(CompileError::new(
709 "bynk.types.empty_refinement",
710 refinement.span,
711 format!(
712 "conflicting exact lengths: `Length({prev})` and `Length({n})` cannot both hold"
713 ),
714 ));
715 }
716 } else {
717 exact_len = Some(*n);
718 }
719 min_len = min_len.max(*n);
720 max_len = max_len.min(*n);
721 }
722 _ => {}
723 }
724 }
725 if min_len > max_len {
726 errors.push(
727 CompileError::new(
728 "bynk.types.empty_refinement",
729 refinement.span,
730 "this refinement has no valid values — minimum length exceeds maximum length",
731 )
732 .with_note(format!(
733 "the effective length range is `{min_len}..={max_len}`, which is empty"
734 )),
735 );
736 }
737}
738
739pub(crate) fn refinement_needs_pin(refinement: &Refinement) -> bool {
747 refinement
748 .predicates
749 .iter()
750 .any(|p| matches!(p.kind, PredKind::Matches(_)))
751}
752
753pub fn zero_value_ts(
756 type_ref: &TypeRef,
757 inline: Option<&Refinement>,
758 types: &HashMap<String, Arc<TypeDecl>>,
759) -> Option<String> {
760 zero_value_ts_inner(type_ref, inline, types, &mut Vec::new())
761}
762
763fn zero_value_ts_inner(
764 type_ref: &TypeRef,
765 inline: Option<&Refinement>,
766 types: &HashMap<String, Arc<TypeDecl>>,
767 visiting: &mut Vec<String>,
768) -> Option<String> {
769 match type_ref {
770 TypeRef::Base(b, _) => {
771 if refinement_admits_zero(*b, inline) {
772 zero_of_base(*b)
773 } else {
774 None
775 }
776 }
777 TypeRef::Option(_, _) => Some("None".to_string()),
779 TypeRef::Named(id) => {
780 let decl = types.get(&id.name)?;
781 match &decl.body {
782 TypeBody::Refined {
783 base, refinement, ..
784 } => {
785 if refinement_admits_zero(*base, refinement.as_ref()) {
786 zero_of_base(*base)
787 } else {
788 None
789 }
790 }
791 TypeBody::Record(rec) => {
792 if visiting.iter().any(|n| n == &id.name) {
798 return None;
799 }
800 visiting.push(id.name.clone());
801 let z = agent_state_zero_record(&rec.fields, types, visiting);
802 visiting.pop();
803 z
804 }
805 TypeBody::Sum(_) | TypeBody::Opaque { .. } => None,
807 }
808 }
809 _ => None,
812 }
813}
814
815fn agent_state_zero_record(
818 fields: &[RecordField],
819 types: &HashMap<String, Arc<TypeDecl>>,
820 visiting: &mut Vec<String>,
821) -> Option<String> {
822 let mut parts = Vec::new();
823 for f in fields {
824 let z = zero_value_ts_inner(&f.type_ref, f.refinement.as_ref(), types, visiting)?;
825 parts.push(format!("{}: {}", f.name.name, z));
826 }
827 Some(format!("{{ {} }}", parts.join(", ")))
828}
829
830fn zero_of_base(b: BaseType) -> Option<String> {
831 Some(
832 match b {
833 BaseType::Int => "0",
834 BaseType::Bool => "false",
835 BaseType::String => "\"\"",
836 BaseType::Float => "0",
837 BaseType::Duration | BaseType::Instant => "0",
840 BaseType::Bytes => "new globalThis.Uint8Array()",
843 }
844 .to_string(),
845 )
846}
847
848fn refinement_admits_zero(base: BaseType, refinement: Option<&Refinement>) -> bool {
853 let Some(r) = refinement else {
854 return true;
855 };
856 r.predicates.iter().all(|p| pred_admits_zero(base, &p.kind))
857}
858
859fn pred_admits_zero(base: BaseType, k: &PredKind) -> bool {
860 match base {
861 BaseType::Int => match k {
862 PredKind::NonNegative => true,
863 PredKind::Positive => false,
864 PredKind::InRange(lo, hi) => lo.value <= 0 && 0 <= hi.value,
865 _ => false,
867 },
868 BaseType::String => match k {
869 PredKind::Matches(p) => regex_matches_empty(p),
870 PredKind::MinLength(n) => *n <= 0,
871 PredKind::MaxLength(n) => *n >= 0,
872 PredKind::Length(n) => *n == 0,
873 PredKind::NonEmpty => false,
874 _ => false,
876 },
877 BaseType::Bool => true,
879 BaseType::Duration | BaseType::Instant | BaseType::Bytes => true,
882 BaseType::Float => match k {
883 PredKind::NonNegative => true,
884 PredKind::Positive => false,
885 PredKind::InRangeF(lo, hi) => lo.value <= 0.0 && 0.0 <= hi.value,
886 _ => false,
888 },
889 }
890}
891
892fn regex_matches_empty(pattern: &str) -> bool {
896 match regress::Regex::new(&format!("^(?:{pattern})$")) {
897 Ok(re) => re.find("").is_some(),
898 Err(_) => false,
899 }
900}
901
902fn ambiguity_diagnostic(pat: &str, span: Span, refinement: &Refinement) -> Option<CompileError> {
913 use super::regex_ambiguity::{Verdict, analyse};
914 Some(match analyse(pat) {
915 Verdict::Linear => return None,
916 Verdict::Exponential(a, b) => CompileError::new(
917 "bynk.types.catastrophic_regex",
918 span,
919 format!(
920 "the pattern in `Matches(\"{pat}\")` can match the same text in \
921 exponentially many ways, which can cause catastrophic backtracking (ReDoS)"
922 ),
923 )
924 .with_note(format!(
925 "inside a repetition, `{a}` and `{b}` can match the same character on \
926 different paths, so each repetition doubles the ways a near-miss input \
927 is tried; make the alternatives (or repeat counts) inside the loop \
928 unambiguous, e.g. `(a|aa)+` as `a+`"
929 )),
930 Verdict::Unanalysable(why) => CompileError::new(
931 "bynk.types.catastrophic_regex",
932 span,
933 format!(
934 "the pattern in `Matches(\"{pat}\")` cannot be shown free of catastrophic \
935 backtracking (ReDoS): {why}"
936 ),
937 )
938 .with_note(
939 "the boundary check runs this pattern on request input under a backtracking \
940 engine, so a pattern that cannot be analysed is rejected; simplify it",
941 ),
942 Verdict::Polynomial(degree, a, b) => {
943 let bound = refinement
947 .predicates
948 .iter()
949 .filter_map(|p| match p.kind {
950 PredKind::MaxLength(n) | PredKind::Length(n) => Some(n),
951 _ => None,
952 })
953 .min();
954 let largest = largest_safe_bound(degree);
955 let (code, advice) = match bound {
956 Some(n) if n <= largest => (
957 "bynk.types.polynomial_regex_capped",
958 format!(
959 "the refinement bounds the input at {n} characters, which keeps \
960 the cost small, so this is a warning; rewrite the pattern so the \
961 repetitions cannot overlap to silence it"
962 ),
963 ),
964 Some(n) => (
965 "bynk.types.polynomial_regex",
966 format!(
967 "the refinement bounds the input at {n} characters, but at degree \
968 {degree} that still allows about {n}^{degree} steps; lower the \
969 bound to at most {largest}, or rewrite the pattern so the \
970 repetitions cannot overlap"
971 ),
972 ),
973 None => (
974 "bynk.types.polynomial_regex",
975 format!(
976 "add a `MaxLength(n)` predicate with `n` at most {largest} to bound \
977 the input, or rewrite the pattern so the repetitions cannot overlap"
978 ),
979 ),
980 };
981 CompileError::new(
982 code,
983 span,
984 format!(
985 "the pattern in `Matches(\"{pat}\")` can split the same text between \
986 repetitions in many ways, so matching time grows with the input's \
987 length to the power {degree} (ReDoS)"
988 ),
989 )
990 .with_note(format!(
991 "`{a}` and `{b}` repeat in sequence and can match the same characters, as \
992 in `\\d*\\d*`; {advice}"
993 ))
994 }
995 })
996}
997
998const POLYNOMIAL_STEP_CAP: u128 = 10_000_000;
1002
1003fn largest_safe_bound(degree: u32) -> i64 {
1006 let mut n: i64 = 1;
1007 while (n as u128 + 1)
1008 .checked_pow(degree)
1009 .is_some_and(|s| s <= POLYNOMIAL_STEP_CAP)
1010 {
1011 n += 1;
1012 }
1013 n
1014}
1015
1016fn has_nested_unbounded_quantifier(pat: &str) -> bool {
1040 let chars: Vec<char> = pat.chars().collect();
1046 let mut stack: Vec<bool> = vec![false];
1049 let mut pending: Option<bool> = None;
1053 let mut i = 0;
1054
1055 fn fold(pending: &mut Option<bool>, stack: &mut [bool]) {
1059 if pending.take() == Some(true) {
1060 *stack.last_mut().unwrap() = true;
1061 }
1062 }
1063
1064 while i < chars.len() {
1065 match chars[i] {
1066 '\\' => {
1068 fold(&mut pending, &mut stack);
1069 i += 2;
1070 pending = Some(false);
1071 }
1072 '[' => {
1074 fold(&mut pending, &mut stack);
1075 i += 1;
1076 if i < chars.len() && chars[i] == '^' {
1077 i += 1;
1078 }
1079 if i < chars.len() && chars[i] == ']' {
1081 i += 1;
1082 }
1083 while i < chars.len() && chars[i] != ']' {
1084 if chars[i] == '\\' {
1085 i += 1;
1086 }
1087 i += 1;
1088 }
1089 i += 1; pending = Some(false);
1091 }
1092 '(' => {
1093 fold(&mut pending, &mut stack);
1094 stack.push(false);
1095 i += 1;
1096 if i < chars.len() && chars[i] == '?' {
1099 i += 1;
1100 if i < chars.len() && matches!(chars[i], ':' | '=' | '!') {
1101 i += 1;
1102 } else if i < chars.len() && chars[i] == '<' {
1103 i += 1;
1104 if i < chars.len() && matches!(chars[i], '=' | '!') {
1105 i += 1;
1106 } else {
1107 while i < chars.len() && chars[i] != '>' {
1108 i += 1;
1109 }
1110 if i < chars.len() {
1111 i += 1; }
1113 }
1114 }
1115 }
1116 }
1117 ')' => {
1118 fold(&mut pending, &mut stack);
1119 let closed = stack.pop().unwrap_or(false);
1120 i += 1;
1121 pending = Some(closed);
1123 }
1124 '|' => {
1126 fold(&mut pending, &mut stack);
1127 i += 1;
1128 }
1129 '*' | '+' => {
1131 let atom_unbounded = pending.take().unwrap_or(false);
1132 if atom_unbounded {
1133 return true; }
1135 *stack.last_mut().unwrap() = true;
1136 i += 1;
1137 if i < chars.len() && chars[i] == '?' {
1138 i += 1; }
1140 }
1141 '?' => {
1143 if let Some(atom_unbounded) = pending.take() {
1144 if atom_unbounded {
1145 *stack.last_mut().unwrap() = true;
1146 }
1147 i += 1;
1148 if i < chars.len() && chars[i] == '?' {
1149 i += 1; }
1151 } else {
1152 i += 1;
1154 pending = Some(false);
1155 }
1156 }
1157 '{' => {
1158 if let Some((unbounded_q, next)) = parse_brace_quantifier(&chars, i) {
1159 let atom_unbounded = pending.take().unwrap_or(false);
1160 if unbounded_q && atom_unbounded {
1161 return true;
1162 }
1163 if unbounded_q || atom_unbounded {
1164 *stack.last_mut().unwrap() = true;
1165 }
1166 i = next;
1167 if i < chars.len() && chars[i] == '?' {
1168 i += 1; }
1170 } else {
1171 fold(&mut pending, &mut stack);
1173 i += 1;
1174 pending = Some(false);
1175 }
1176 }
1177 _ => {
1179 fold(&mut pending, &mut stack);
1180 i += 1;
1181 pending = Some(false);
1182 }
1183 }
1184 }
1185 false
1186}
1187
1188fn parse_brace_quantifier(chars: &[char], start: usize) -> Option<(bool, usize)> {
1194 let mut i = start + 1;
1195 let lo_start = i;
1196 while i < chars.len() && chars[i].is_ascii_digit() {
1197 i += 1;
1198 }
1199 if i == lo_start {
1200 return None; }
1202 let mut unbounded = false;
1203 if i < chars.len() && chars[i] == ',' {
1204 i += 1;
1205 let hi_start = i;
1206 while i < chars.len() && chars[i].is_ascii_digit() {
1207 i += 1;
1208 }
1209 if i == hi_start {
1210 unbounded = true; }
1212 }
1213 if i < chars.len() && chars[i] == '}' {
1214 Some((unbounded, i + 1))
1215 } else {
1216 None
1217 }
1218}
1219
1220#[cfg(test)]
1221mod redos_tests {
1222 use super::has_nested_unbounded_quantifier as redos;
1223
1224 #[test]
1225 fn flags_nested_unbounded_quantifiers() {
1226 assert!(redos("(a+)+"));
1229 assert!(redos("(a+)+$"));
1230 assert!(redos("(a*)*"));
1231 assert!(redos("(a+)*"));
1232 assert!(redos("(a*)+"));
1233 assert!(redos("((ab)+)+"));
1234 assert!(redos("(a{1,})+")); assert!(redos("(a+){2,}")); assert!(redos("x(y(z+)+w)+")); assert!(redos("(\\d+)+"));
1238 assert!(redos("(?:a+)+")); assert!(redos("([a-z]+)*")); assert!(redos("((a+)?)+")); assert!(redos("(a+|b)+")); }
1243
1244 #[test]
1245 fn allows_safe_patterns() {
1246 assert!(!redos("a+"));
1248 assert!(!redos("(a+)")); assert!(!redos("(a+)(b+)")); assert!(!redos("(ab)+")); assert!(!redos("(a+)?")); assert!(!redos("(a+){2,3}")); assert!(!redos("(a{2,3})+"));
1256 assert!(!redos("[a-z]+")); assert!(!redos("a{2,}b{2,}")); }
1259
1260 #[test]
1261 fn the_ambiguity_analysis_covers_what_the_first_pass_does_not() {
1262 use super::super::regex_ambiguity::{Verdict, analyse};
1266 for pat in [
1267 "(a|a)+",
1268 "(\\d|\\d\\d)+",
1269 "(a|aa)*",
1270 "(a{1,2})+",
1271 "(a{2,3})+",
1272 ] {
1273 assert!(!redos(pat), "the first pass is not meant to flag `{pat}`");
1274 assert!(
1275 matches!(analyse(pat), Verdict::Exponential(..)),
1276 "`{pat}` is exponential"
1277 );
1278 }
1279 assert!(matches!(analyse("\\d*\\d*"), Verdict::Polynomial(..)));
1280 assert_eq!(analyse("(foo|foobar)+"), Verdict::Linear);
1281 }
1282
1283 #[test]
1284 fn allows_every_pattern_used_in_the_repo() {
1285 for pat in [
1288 "[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}",
1289 "[a-z][a-z0-9-]*",
1290 "[A-Z]{3}-[0-9]{4}",
1291 "[A-Z]{3}",
1292 "[a-z]+",
1293 "[A-Z]+",
1294 "[a-z]+(?<=ing)",
1295 "[a-z0-9_]+",
1296 "[a-z0-9]{1,16}",
1297 "[a-z0-9]{1,8}",
1298 "[A-Z0-9]{3,16}",
1299 "[a-z0-9]{3,8}",
1300 "[a-zA-Z0-9]{6,8}",
1301 "ab|cd",
1302 "AUTH-[0-9]{8}",
1303 "AUTH-[0-9]+",
1304 "CUST-[0-9]+",
1305 "https?://.+",
1306 "ORD-[0-9]{6}",
1307 "ORD-[0-9]+",
1308 "SHP-[0-9]{8}",
1309 "T-[0-9]+",
1310 "[A-Z0-9]{8}",
1311 "[A-Z]{2}[0-9]{2}",
1312 "^[a-z]+$",
1313 "\\d{4}",
1314 "\\d+",
1315 ] {
1316 assert!(!redos(pat), "false positive on safe pattern `{pat}`");
1317 assert_eq!(
1318 super::super::regex_ambiguity::analyse(pat),
1319 super::super::regex_ambiguity::Verdict::Linear,
1320 "the ambiguity analysis rejects safe pattern `{pat}`"
1321 );
1322 }
1323 let locale_pat = super::locale_tag_pattern().expect("LocaleTag has a Matches predicate");
1327 assert!(
1328 !redos(locale_pat),
1329 "false positive on safe pattern `{locale_pat}`"
1330 );
1331 assert_eq!(
1332 super::super::regex_ambiguity::analyse(locale_pat),
1333 super::super::regex_ambiguity::Verdict::Linear,
1334 "the ambiguity analysis rejects `LocaleTag`"
1335 );
1336 }
1337}
1338
1339#[cfg(test)]
1340mod locale_tag_tests {
1341 use super::locale_tag_accepts;
1342
1343 #[test]
1344 fn admits_the_pre_existing_shapes() {
1345 for tag in ["en", "pt-BR", "zh-Hans-CN", "es-419"] {
1346 assert!(locale_tag_accepts(tag), "expected `{tag}` to be admitted");
1347 }
1348 }
1349
1350 #[test]
1351 fn admits_variants_extensions_private_use_and_extlang() {
1352 for tag in [
1353 "de-CH-1996", "ca-valencia", "sl-rozaj", "en-scotland-fonipa", "en-US-u-ca-buddhist", "de-CH-x-phonebk", "x-custom", "zh-yue", ] {
1362 assert!(locale_tag_accepts(tag), "expected `{tag}` to be admitted");
1363 }
1364 }
1365
1366 #[test]
1367 fn rejects_grandfathered_and_malformed_tags() {
1368 for tag in [
1369 "i-klingon", "en-GB-oed", "Klingon", "pt-br", "en-SCOTLAND", "en-x-abcdefghi", "x", "",
1377 ] {
1378 assert!(!locale_tag_accepts(tag), "expected `{tag}` to be rejected");
1379 }
1380 }
1381}