1use super::*;
7
8pub(crate) fn check_type_decl(
9 t: &TypeDecl,
10 types: &HashMap<String, TypeDecl>,
11 errors: &mut Vec<CompileError>,
12) {
13 match &t.body {
14 TypeBody::Refined {
15 base,
16 base_span,
17 refinement,
18 } => {
19 check_refinement(*base, *base_span, refinement.as_ref(), errors);
20 }
21 TypeBody::Opaque {
22 base,
23 base_span,
24 refinement,
25 } => {
26 check_refinement(*base, *base_span, refinement.as_ref(), errors);
28 }
29 TypeBody::Record(r) => {
30 for f in &r.fields {
31 if let Some(ref_r) = &f.refinement {
32 if let Some(b) = field_base_type(&f.type_ref, types) {
34 check_refinement(b, f.type_ref.span(), Some(ref_r), errors);
35 } else {
36 errors.push(CompileError::new(
37 "bynk.types.field_refinement_not_base",
38 ref_r.span,
39 format!(
40 "inline refinement on field `{}` requires a base or refined type",
41 f.name.name
42 ),
43 ));
44 }
45 }
46 }
47 }
48 TypeBody::Sum(_) => {
49 }
51 }
52}
53
54fn field_base_type(r: &TypeRef, types: &HashMap<String, TypeDecl>) -> Option<BaseType> {
56 match r {
57 TypeRef::Base(b, _) => Some(*b),
58 TypeRef::Named(id) => match types.get(&id.name).map(|t| &t.body) {
59 Some(TypeBody::Refined { base, .. }) => Some(*base),
60 _ => None,
61 },
62 _ => None,
63 }
64}
65
66pub(crate) fn type_decl_base(decl: &TypeDecl) -> Option<BaseType> {
70 match &decl.body {
71 TypeBody::Refined { base, .. } => Some(*base),
72 TypeBody::Opaque { base, .. } => Some(*base),
73 _ => None,
74 }
75}
76
77pub(crate) fn type_decl_refinement(decl: &TypeDecl) -> Option<&Refinement> {
79 match &decl.body {
80 TypeBody::Refined { refinement, .. } | TypeBody::Opaque { refinement, .. } => {
81 refinement.as_ref()
82 }
83 _ => None,
84 }
85}
86
87pub(crate) fn const_literal(e: &Expr) -> Option<ConstLit> {
92 match &e.kind {
93 ExprKind::IntLit(n) => Some(ConstLit::Int(*n)),
94 ExprKind::FloatLit { value, .. } => Some(ConstLit::Float(*value)),
95 ExprKind::StrLit(s) => Some(ConstLit::Str(s.clone())),
96 ExprKind::BoolLit(b) => Some(ConstLit::Bool(*b)),
97 ExprKind::UnitLit => Some(ConstLit::Unit),
98 ExprKind::UnaryOp(UnaryOp::Neg, inner) => match &inner.kind {
99 ExprKind::IntLit(n) => Some(ConstLit::Int(n.checked_neg()?)),
100 ExprKind::FloatLit { value, .. } => Some(ConstLit::Float(-*value)),
101 _ => None,
102 },
103 _ => None,
104 }
105}
106
107pub(crate) fn eval_predicate(pred: &PredKind, lit: &ConstLit) -> bool {
114 match (pred, lit) {
115 (PredKind::NonNegative, ConstLit::Int(n)) => *n >= 0,
116 (PredKind::Positive, ConstLit::Int(n)) => *n > 0,
117 (PredKind::InRange(lo, hi), ConstLit::Int(n)) => lo.value <= *n && *n <= hi.value,
118 (PredKind::NonNegative, ConstLit::Float(v)) => *v >= 0.0,
119 (PredKind::Positive, ConstLit::Float(v)) => *v > 0.0,
120 (PredKind::InRangeF(lo, hi), ConstLit::Float(v)) => lo.value <= *v && *v <= hi.value,
121 (PredKind::MinLength(k), ConstLit::Str(s)) => s.chars().count() as i64 >= *k,
122 (PredKind::MaxLength(k), ConstLit::Str(s)) => (s.chars().count() as i64) <= *k,
123 (PredKind::Length(k), ConstLit::Str(s)) => s.chars().count() as i64 == *k,
124 (PredKind::NonEmpty, ConstLit::Str(s)) => !s.is_empty(),
125 (PredKind::Matches(pat), ConstLit::Str(s)) => Regex::new(&format!("^(?:{pat})$"))
126 .map(|re| re.is_match(s))
127 .unwrap_or(false),
128 _ => true,
129 }
130}
131
132pub(crate) fn first_failed_predicate<'a>(
134 refinement: &'a Refinement,
135 lit: &ConstLit,
136) -> Option<&'a PredKind> {
137 for p in &refinement.predicates {
138 if !eval_predicate(&p.kind, lit) {
139 return Some(&p.kind);
140 }
141 }
142 None
143}
144
145pub(crate) fn literal_matches_base(lit: &ConstLit, base: BaseType) -> bool {
146 matches!(
147 (lit, base),
148 (ConstLit::Int(_), BaseType::Int)
149 | (ConstLit::Str(_), BaseType::String)
150 | (ConstLit::Bool(_), BaseType::Bool)
151 | (ConstLit::Float(_), BaseType::Float)
152 )
153}
154
155pub(crate) fn admit_refined_literal(
164 expr: &Expr,
165 expected: Option<&Ty>,
166 ctx: &mut Ctx,
167) -> Option<Ty> {
168 let Some(Ty::Named {
169 name,
170 kind: NamedKind::Refined(base),
171 }) = expected
172 else {
173 return None;
174 };
175 let lit = const_literal(expr)?;
176 if !literal_matches_base(&lit, *base) {
177 return None;
178 }
179 let decl = ctx.input.types.get(name)?.clone();
180 if let Some(refinement) = type_decl_refinement(&decl)
181 && let Some(failed) = first_failed_predicate(refinement, &lit)
182 {
183 ctx.errors.push(CompileError::new(
184 "bynk.refine.literal_violates",
185 expr.span,
186 format!(
187 "literal {} does not satisfy `{}` required by type `{}`",
188 lit.display(),
189 failed.name(),
190 name
191 ),
192 ));
193 }
194 Some(named_ty(&decl))
195}
196
197fn check_refinement(
198 base: BaseType,
199 base_span: Span,
200 refinement: Option<&Refinement>,
201 errors: &mut Vec<CompileError>,
202) {
203 let Some(refinement) = refinement else {
204 return;
205 };
206
207 for pred in &refinement.predicates {
208 if !pred_applies_to(&pred.kind, base) {
209 let numeric_bound_mismatch = matches!(
213 (&pred.kind, base),
214 (PredKind::InRange(_, _), BaseType::Float)
215 | (PredKind::InRangeF(_, _), BaseType::Int)
216 );
217 if numeric_bound_mismatch {
218 let (bounds, want) = if base == BaseType::Float {
219 ("`Int`", "`InRange(0.0, 1.0)`")
220 } else {
221 ("`Float`", "`InRange(0, 1)`")
222 };
223 errors.push(
224 CompileError::new(
225 "bynk.types.no_numeric_coercion",
226 pred.span,
227 format!(
228 "`InRange` bounds are {bounds} literals, but the base type is `{}`",
229 base.name()
230 ),
231 )
232 .with_label(
233 base_span,
234 format!("base type `{}` declared here", base.name()),
235 )
236 .with_note(format!(
237 "refinement bounds must match the base type — e.g. {want}"
238 )),
239 );
240 continue;
241 }
242 errors.push(
243 CompileError::new(
244 "bynk.types.predicate_base_mismatch",
245 pred.span,
246 format!(
247 "predicate `{}` cannot be applied to base type `{}`",
248 pred.kind.name(),
249 base.name()
250 ),
251 )
252 .with_label(
253 base_span,
254 format!("base type `{}` declared here", base.name()),
255 )
256 .with_note(predicate_base_help(pred.kind.name())),
257 );
258 }
259 match &pred.kind {
260 PredKind::Matches(pat) => {
261 if let Err(e) = Regex::new(pat) {
262 errors.push(
263 CompileError::new(
264 "bynk.types.invalid_regex",
265 pred.span,
266 format!("invalid regular expression in `Matches(\"{pat}\")`"),
267 )
268 .with_note(format!("regex parse error: {e}")),
269 );
270 }
271 }
272 PredKind::InRange(lo, hi) => {
273 if lo.value > hi.value {
274 errors.push(
275 CompileError::new(
276 "bynk.types.inverted_range",
277 pred.span,
278 format!(
279 "`InRange({}, {})` has its bounds inverted (`min` must be ≤ `max`)",
280 lo.value, hi.value
281 ),
282 )
283 .with_note("swap the arguments, e.g. `InRange(min, max)`")
284 .with_suggestion(
287 "swap the bounds",
288 vec![
289 (lo.span, hi.value.to_string()),
290 (hi.span, lo.value.to_string()),
291 ],
292 Applicability::MachineApplicable,
293 ),
294 );
295 }
296 }
297 PredKind::InRangeF(lo, hi) => {
298 if lo.value > hi.value {
299 errors.push(
300 CompileError::new(
301 "bynk.types.inverted_range",
302 pred.span,
303 format!(
304 "`InRange({}, {})` has its bounds inverted (`min` must be ≤ `max`)",
305 lo.lexeme, hi.lexeme
306 ),
307 )
308 .with_note("swap the arguments, e.g. `InRange(min, max)`")
309 .with_suggestion(
310 "swap the bounds",
311 vec![(lo.span, hi.lexeme.clone()), (hi.span, lo.lexeme.clone())],
312 Applicability::MachineApplicable,
313 ),
314 );
315 }
316 }
317 PredKind::MinLength(n) | PredKind::MaxLength(n) | PredKind::Length(n) => {
318 if *n < 0 {
319 errors.push(CompileError::new(
320 "bynk.types.negative_length",
321 pred.span,
322 format!("length argument must be non-negative, got {n}"),
323 ));
324 }
325 }
326 PredKind::NonNegative | PredKind::Positive | PredKind::NonEmpty => {}
327 }
328 }
329
330 let all_compatible = refinement
331 .predicates
332 .iter()
333 .all(|p| pred_applies_to(&p.kind, base));
334 if !all_compatible {
335 return;
336 }
337 match base {
338 BaseType::Int => check_int_refinement_consistency(refinement, errors),
339 BaseType::String => check_string_refinement_consistency(refinement, errors),
340 BaseType::Bool => {}
341 BaseType::Float => check_float_refinement_consistency(refinement, errors),
342 BaseType::Duration | BaseType::Instant | BaseType::Bytes => {}
347 }
348}
349
350fn pred_applies_to(pred: &PredKind, base: BaseType) -> bool {
351 matches!(
352 (pred, base),
353 (PredKind::Matches(_), BaseType::String)
354 | (PredKind::InRange(_, _), BaseType::Int)
355 | (PredKind::InRangeF(_, _), BaseType::Float)
356 | (PredKind::MinLength(_), BaseType::String)
357 | (PredKind::MaxLength(_), BaseType::String)
358 | (PredKind::Length(_), BaseType::String)
359 | (PredKind::NonNegative, BaseType::Int | BaseType::Float)
360 | (PredKind::Positive, BaseType::Int | BaseType::Float)
361 | (PredKind::NonEmpty, BaseType::String)
362 )
363}
364
365fn predicate_base_help(name: &str) -> &'static str {
366 match name {
367 "Matches" | "MinLength" | "MaxLength" | "Length" | "NonEmpty" => {
368 "this predicate applies to `String` only"
369 }
370 "NonNegative" | "Positive" => "this predicate applies to `Int` and `Float` only",
371 "InRange" => {
372 "this predicate applies to `Int` and `Float` only, with bounds matching the base"
373 }
374 _ => "see the documentation for valid predicate-base combinations",
375 }
376}
377
378pub(crate) fn check_int_refinement_consistency(
379 refinement: &Refinement,
380 errors: &mut Vec<CompileError>,
381) {
382 let mut lo: i64 = i64::MIN;
383 let mut hi: i64 = i64::MAX;
384 for p in &refinement.predicates {
385 match &p.kind {
386 PredKind::Positive => lo = lo.max(1),
387 PredKind::NonNegative => lo = lo.max(0),
388 PredKind::InRange(a, b) => {
389 lo = lo.max(a.value);
390 hi = hi.min(b.value);
391 }
392 _ => {}
393 }
394 }
395 if lo > hi {
396 errors.push(
397 CompileError::new(
398 "bynk.types.empty_refinement",
399 refinement.span,
400 "this refinement has no valid values — the predicates contradict each other",
401 )
402 .with_note(format!(
403 "the effective range is `{lo}..={hi}`, which is empty"
404 )),
405 );
406 }
407}
408
409pub(crate) fn check_float_refinement_consistency(
410 refinement: &Refinement,
411 errors: &mut Vec<CompileError>,
412) {
413 let mut lo = f64::NEG_INFINITY;
414 let mut hi = f64::INFINITY;
415 let mut lo_exclusive = false;
417 for p in &refinement.predicates {
418 match &p.kind {
419 PredKind::Positive if 0.0 >= lo => {
420 lo = 0.0;
421 lo_exclusive = true;
422 }
423 PredKind::NonNegative if 0.0 > lo => {
424 lo = 0.0;
425 lo_exclusive = false;
426 }
427 PredKind::InRangeF(a, b) => {
428 if a.value > lo {
429 lo = a.value;
430 lo_exclusive = false;
431 }
432 hi = hi.min(b.value);
433 }
434 _ => {}
435 }
436 }
437 if lo > hi || (lo == hi && lo_exclusive) {
438 errors.push(
439 CompileError::new(
440 "bynk.types.empty_refinement",
441 refinement.span,
442 "this refinement has no valid values — the predicates contradict each other",
443 )
444 .with_note(format!(
445 "the effective range is `{lo}..={hi}`{}, which is empty",
446 if lo_exclusive {
447 " (lower bound exclusive)"
448 } else {
449 ""
450 }
451 )),
452 );
453 }
454}
455
456pub(crate) fn check_string_refinement_consistency(
457 refinement: &Refinement,
458 errors: &mut Vec<CompileError>,
459) {
460 let mut min_len: i64 = 0;
461 let mut max_len: i64 = i64::MAX;
462 let mut exact_len: Option<i64> = None;
463 for p in &refinement.predicates {
464 match &p.kind {
465 PredKind::MinLength(n) => min_len = min_len.max(*n),
466 PredKind::MaxLength(n) => max_len = max_len.min(*n),
467 PredKind::NonEmpty => min_len = min_len.max(1),
468 PredKind::Length(n) => {
469 if let Some(prev) = exact_len {
470 if prev != *n {
471 errors.push(CompileError::new(
472 "bynk.types.empty_refinement",
473 refinement.span,
474 format!(
475 "conflicting exact lengths: `Length({prev})` and `Length({n})` cannot both hold"
476 ),
477 ));
478 }
479 } else {
480 exact_len = Some(*n);
481 }
482 min_len = min_len.max(*n);
483 max_len = max_len.min(*n);
484 }
485 _ => {}
486 }
487 }
488 if min_len > max_len {
489 errors.push(
490 CompileError::new(
491 "bynk.types.empty_refinement",
492 refinement.span,
493 "this refinement has no valid values — minimum length exceeds maximum length",
494 )
495 .with_note(format!(
496 "the effective length range is `{min_len}..={max_len}`, which is empty"
497 )),
498 );
499 }
500}
501
502pub(crate) fn refinement_needs_pin(refinement: &Refinement) -> bool {
510 refinement
511 .predicates
512 .iter()
513 .any(|p| matches!(p.kind, PredKind::Matches(_)))
514}
515
516pub fn zero_value_ts(
519 type_ref: &TypeRef,
520 inline: Option<&Refinement>,
521 types: &HashMap<String, TypeDecl>,
522) -> Option<String> {
523 match type_ref {
524 TypeRef::Base(b, _) => {
525 if refinement_admits_zero(*b, inline) {
526 zero_of_base(*b)
527 } else {
528 None
529 }
530 }
531 TypeRef::Option(_, _) => Some("None".to_string()),
533 TypeRef::Named(id) => {
534 let decl = types.get(&id.name)?;
535 match &decl.body {
536 TypeBody::Refined {
537 base, refinement, ..
538 } => {
539 if refinement_admits_zero(*base, refinement.as_ref()) {
540 zero_of_base(*base)
541 } else {
542 None
543 }
544 }
545 TypeBody::Record(rec) => agent_state_zero_record(&rec.fields, types),
546 TypeBody::Sum(_) | TypeBody::Opaque { .. } => None,
548 }
549 }
550 _ => None,
553 }
554}
555
556pub fn agent_state_zero_record(
559 fields: &[RecordField],
560 types: &HashMap<String, TypeDecl>,
561) -> Option<String> {
562 let mut parts = Vec::new();
563 for f in fields {
564 let z = zero_value_ts(&f.type_ref, f.refinement.as_ref(), types)?;
565 parts.push(format!("{}: {}", f.name.name, z));
566 }
567 Some(format!("{{ {} }}", parts.join(", ")))
568}
569
570fn zero_of_base(b: BaseType) -> Option<String> {
571 Some(
572 match b {
573 BaseType::Int => "0",
574 BaseType::Bool => "false",
575 BaseType::String => "\"\"",
576 BaseType::Float => "0",
577 BaseType::Duration | BaseType::Instant => "0",
580 BaseType::Bytes => "new Uint8Array()",
583 }
584 .to_string(),
585 )
586}
587
588fn refinement_admits_zero(base: BaseType, refinement: Option<&Refinement>) -> bool {
593 let Some(r) = refinement else {
594 return true;
595 };
596 r.predicates.iter().all(|p| pred_admits_zero(base, &p.kind))
597}
598
599fn pred_admits_zero(base: BaseType, k: &PredKind) -> bool {
600 match base {
601 BaseType::Int => match k {
602 PredKind::NonNegative => true,
603 PredKind::Positive => false,
604 PredKind::InRange(lo, hi) => lo.value <= 0 && 0 <= hi.value,
605 _ => false,
607 },
608 BaseType::String => match k {
609 PredKind::Matches(p) => regex_matches_empty(p),
610 PredKind::MinLength(n) => *n <= 0,
611 PredKind::MaxLength(n) => *n >= 0,
612 PredKind::Length(n) => *n == 0,
613 PredKind::NonEmpty => false,
614 _ => false,
616 },
617 BaseType::Bool => true,
619 BaseType::Duration | BaseType::Instant | BaseType::Bytes => true,
622 BaseType::Float => match k {
623 PredKind::NonNegative => true,
624 PredKind::Positive => false,
625 PredKind::InRangeF(lo, hi) => lo.value <= 0.0 && 0.0 <= hi.value,
626 _ => false,
628 },
629 }
630}
631
632fn regex_matches_empty(pattern: &str) -> bool {
635 match Regex::new(&format!("^(?:{pattern})$")) {
636 Ok(re) => re.is_match(""),
637 Err(_) => false,
638 }
639}