Skip to main content

bynk_check/checker/
refinements.rs

1//! Refinement, literal, and zero-value logic.
2//!
3//! Split out of `checker.rs` (v0.29.10) verbatim; the parent module
4//! re-exports these via `use refinements::*`.
5
6use 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            // Opaque types share refinement-validity rules with refined types.
27            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                    // Inline refinements on fields must apply to the field's base type.
33                    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            // No further per-variant checks at the type level.
50        }
51    }
52}
53
54/// The base type of a field's type-ref (chasing through named refined types).
55fn 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
66/// The implicit base type of a TypeDecl whose constructor would be `T.of`:
67/// Refined and Opaque types alike share the `of(base) -> Result[T, _]` shape.
68/// Returns None for record / sum types.
69pub(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
77/// The refinement attached to a refined or opaque type declaration, if any.
78pub(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
87/// Extract a compile-time literal from an expression, if it is one v0.9.4's
88/// static refinement check accepts: an int/string/bool/unit literal, or a unary
89/// minus applied directly to an int literal. Anything else (arithmetic, idents,
90/// calls) is not statically evaluated and keeps the runtime `Result` path.
91pub(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
107/// Evaluate a single predicate against a constant literal. A predicate whose
108/// expected base type doesn't match the literal (e.g. a length predicate on an
109/// int) returns `true` here — the base/predicate mismatch is a declaration-time
110/// error reported by `check_refinement`, not a construction concern. String
111/// length is measured in Unicode scalar values, which agrees with JS `.length`
112/// for the BMP (the range fixtures use ASCII).
113pub(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
132/// The first predicate the literal fails, or `None` if it satisfies them all.
133pub(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
155/// v0.9.4: expected-type-directed literal admission. When a position expects a
156/// **refined** type `T` and `expr` is a compile-time literal of `T`'s base, the
157/// literal takes the type `T` directly (the emitter lowers it to
158/// `T.unsafe(...)`); a literal that violates the refinement is a compile error.
159/// Returns `None` when no refined type is expected (so the caller keeps the
160/// literal's base type) — `.of` remains the only constructor for runtime values.
161/// Opaque types are intentionally excluded: their representation is hidden, so
162/// they are still built via `T.of(...)`.
163pub(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            // v0.21: `InRange` bounds must match the numeric base type —
210            // `Float where InRange(0, 1)` is the no-coercion rule applied
211            // to refinement bounds, not a predicate/base mismatch.
212            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                        // v0.40 (ADR 0073): a machine-applicable swap — replace
285                        // each bound's text with the other's, in place.
286                        .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        // v0.86/v0.90/v0.110: no refinement predicate applies to `Duration`,
343        // `Instant`, or `Bytes` (none is in any `pred_applies_to` row), so a
344        // refined one is rejected upstream and there is nothing to
345        // consistency-check here.
346        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    // `Positive` excludes the lower endpoint (0.0 itself is not positive).
416    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
502// -- function body type checking --
503
504/// v0.9.1: `assert e` as an expression. Test-privileged. Requires `e : Bool`.
505/// Always yields type `()`.
506/// True if a refinement cannot be satisfied by a generated default value — i.e.
507/// it contains a `Matches` predicate, where bare `Val[T]` must be given an
508/// explicit pin instead.
509pub(crate) fn refinement_needs_pin(refinement: &Refinement) -> bool {
510    refinement
511        .predicates
512        .iter()
513        .any(|p| matches!(p.kind, PredKind::Matches(_)))
514}
515
516/// The TypeScript zero-value expression for `type_ref` (with an optional
517/// inline field refinement), or `None` if the type is not zeroable.
518pub 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        // Option's zero is None, regardless of the inner type.
532        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                // Non-Option sum types and opaque types have no defined zero.
547                TypeBody::Sum(_) | TypeBody::Opaque { .. } => None,
548            }
549        }
550        // Result / Effect / HttpResult / ValidationError / Unit are not
551        // admissible state-field types and have no zero.
552        _ => None,
553    }
554}
555
556/// The zero record `{ f₁: z₁, …, fₙ: zₙ }` for a set of fields, or `None` if
557/// any field is not zeroable.
558pub 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            // v0.86/v0.90: a `Duration` is milliseconds and an `Instant` is
578            // epoch milliseconds; the zero of each is `0` (the Unix epoch).
579            BaseType::Duration | BaseType::Instant => "0",
580            // v0.110 (ADR 0142): the zero of `Bytes` is the empty octet
581            // sequence (`""` in base64), erased to an empty `Uint8Array`.
582            BaseType::Bytes => "new Uint8Array()",
583        }
584        .to_string(),
585    )
586}
587
588/// Whether the zero value of `base` satisfies every predicate in `refinement`.
589/// Conservative: any predicate we cannot prove admits the zero returns false,
590/// surfacing the `non_zeroable_state_field` diagnostic rather than risking an
591/// invalid fresh state.
592fn 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            // Length/Matches predicates don't apply to Int; reject conservatively.
606            _ => 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            // Numeric predicates don't apply to String; reject conservatively.
615            _ => false,
616        },
617        // The only Bool zero is `false`; no Bool refinement predicates exist.
618        BaseType::Bool => true,
619        // No refinement predicate applies to `Duration`, `Instant`, or
620        // `Bytes`, so the question is vacuous — admit it (mirrors `Bool`).
621        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            // Other predicates don't apply to Float; reject conservatively.
627            _ => false,
628        },
629    }
630}
631
632/// Does the refinement pattern match the empty string? Anchored exactly as the
633/// emitted refined-type constructor anchors it (`^(?:pattern)$`).
634fn regex_matches_empty(pattern: &str) -> bool {
635    match Regex::new(&format!("^(?:{pattern})$")) {
636        Ok(re) => re.is_match(""),
637        Err(_) => false,
638    }
639}