Skip to main content

mago_codex/
assertion.rs

1use std::borrow::Cow;
2use std::hash::BuildHasher;
3use std::hash::Hash;
4
5use foldhash::fast::FixedState;
6
7use mago_word::Word;
8use mago_word::concat_word;
9use mago_word::i64_word;
10use mago_word::usize_word;
11use mago_word::word;
12
13use crate::metadata::CodebaseMetadata;
14use crate::ttype::TType;
15use crate::ttype::atomic::TAtomic;
16use crate::ttype::atomic::array::key::ArrayKey;
17use crate::ttype::template::TemplateResult;
18use crate::ttype::template::inferred_type_replacer;
19use crate::ttype::union::TUnion;
20
21#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
22#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
23pub enum Assertion {
24    Any,
25    IsType(TAtomic),
26    IsNotType(TAtomic),
27    Falsy,
28    Truthy,
29    IsIdentical(TAtomic),
30    IsNotIdentical(TAtomic),
31    IsEqual(TAtomic),
32    IsNotEqual(TAtomic),
33    IsEqualIsset,
34    IsIsset,
35    IsNotIsset,
36    HasStringArrayAccess,
37    HasIntOrStringArrayAccess,
38    ArrayKeyExists,
39    ArrayKeyDoesNotExist,
40    InArray(TUnion),
41    NotInArray(TUnion),
42    HasArrayKey(ArrayKey),
43    DoesNotHaveArrayKey(ArrayKey),
44    HasNonnullEntryForKey(ArrayKey),
45    DoesNotHaveNonnullEntryForKey(ArrayKey),
46    Empty,
47    NonEmpty,
48    NonEmptyCountable(bool),
49    EmptyCountable,
50    HasExactCount(usize),
51    HasAtLeastCount(usize),
52    DoesNotHaveExactCount(usize),
53    DoesNotHasAtLeastCount(usize),
54    IsLessThan(i64),
55    IsLessThanOrEqual(i64),
56    IsGreaterThan(i64),
57    IsGreaterThanOrEqual(i64),
58    /// A range fact implied by a comparison with a non-literal bound.
59    ///
60    /// For example, `$a > $b` with `int<0, max> $b` implies `$a > 0`
61    /// in the true branch, but its negation does not imply `$a <= 0`.
62    /// These assertions therefore deliberately negate to `Any`.
63    IsLessThanFromBound(i64),
64    IsLessThanOrEqualFromBound(i64),
65    IsGreaterThanFromBound(i64),
66    IsGreaterThanOrEqualFromBound(i64),
67    /// An exact comparison with another tracked expression.
68    ///
69    /// Unlike bound-derived facts, these retain the relationship so consumers
70    /// can reason about arithmetic such as `$limit - $length` after
71    /// `$length < $limit`.
72    IsLessThanVariable(Word),
73    IsLessThanOrEqualVariable(Word),
74    IsGreaterThanVariable(Word),
75    IsGreaterThanOrEqualVariable(Word),
76    Countable,
77    NotCountable(bool),
78}
79
80impl Assertion {
81    #[must_use]
82    pub fn to_atom(&self) -> Word {
83        match self {
84            Assertion::Any => word("any"),
85            Assertion::Falsy => word("falsy"),
86            Assertion::Truthy => word("truthy"),
87            Assertion::IsEqualIsset => word("=isset"),
88            Assertion::IsIsset => word("isset"),
89            Assertion::IsNotIsset => word("!isset"),
90            Assertion::HasStringArrayAccess => word("=string-array-access"),
91            Assertion::HasIntOrStringArrayAccess => word("=int-or-string-array-access"),
92            Assertion::ArrayKeyExists => word("array-key-exists"),
93            Assertion::ArrayKeyDoesNotExist => word("!array-key-exists"),
94            Assertion::EmptyCountable => word("empty-countable"),
95            Assertion::Empty => word("empty"),
96            Assertion::NonEmpty => word("non-empty"),
97            Assertion::Countable => word("countable"),
98            Assertion::NotCountable(_) => word("!countable"),
99            Assertion::IsType(atomic) => atomic.get_id(),
100            Assertion::IsNotType(atomic) => concat_word!(b"!", atomic.get_id()),
101            Assertion::IsIdentical(atomic) => concat_word!(b"=", atomic.get_id()),
102            Assertion::IsNotIdentical(atomic) => concat_word!(b"!=", atomic.get_id()),
103            Assertion::IsEqual(atomic) => concat_word!(b"~", atomic.get_id()),
104            Assertion::IsNotEqual(atomic) => concat_word!(b"!~", atomic.get_id()),
105            Assertion::InArray(union) => concat_word!(b"=in-array-", union.get_id()),
106            Assertion::NotInArray(union) => concat_word!(b"!=in-array-", union.get_id()),
107            Assertion::HasArrayKey(key) => concat_word!(b"=has-array-key-", key.to_atom()),
108            Assertion::DoesNotHaveArrayKey(key) => concat_word!(b"!=has-array-key-", key.to_atom()),
109            Assertion::HasNonnullEntryForKey(key) => concat_word!(b"=has-nonnull-entry-for-", key.to_atom()),
110            Assertion::DoesNotHaveNonnullEntryForKey(key) => {
111                concat_word!(b"!=has-nonnull-entry-for-", key.to_atom())
112            }
113            Assertion::HasExactCount(number) => concat_word!(b"has-exactly-", usize_word(*number)),
114            Assertion::HasAtLeastCount(number) => concat_word!(b"has-at-least-", usize_word(*number)),
115            Assertion::DoesNotHaveExactCount(number) => concat_word!(b"!has-exactly-", usize_word(*number)),
116            Assertion::DoesNotHasAtLeastCount(number) => concat_word!(b"has-at-most-", usize_word(*number)),
117            Assertion::IsLessThan(number) => concat_word!(b"is-less-than-", i64_word(*number)),
118            Assertion::IsLessThanOrEqual(number) => concat_word!(b"is-less-than-or-equal-", i64_word(*number)),
119            Assertion::IsGreaterThan(number) => concat_word!(b"is-greater-than-", i64_word(*number)),
120            Assertion::IsGreaterThanOrEqual(number) => concat_word!(b"is-greater-than-or-equal-", i64_word(*number)),
121            Assertion::IsLessThanFromBound(number) => concat_word!(b"is-less-than-from-bound-", i64_word(*number)),
122            Assertion::IsLessThanOrEqualFromBound(number) => {
123                concat_word!(b"is-less-than-or-equal-from-bound-", i64_word(*number))
124            }
125            Assertion::IsGreaterThanFromBound(number) => {
126                concat_word!(b"is-greater-than-from-bound-", i64_word(*number))
127            }
128            Assertion::IsGreaterThanOrEqualFromBound(number) => {
129                concat_word!(b"is-greater-than-or-equal-from-bound-", i64_word(*number))
130            }
131            Assertion::IsLessThanVariable(variable) => concat_word!(b"is-less-than-variable-", variable),
132            Assertion::IsLessThanOrEqualVariable(variable) => {
133                concat_word!(b"is-less-than-or-equal-variable-", variable)
134            }
135            Assertion::IsGreaterThanVariable(variable) => {
136                concat_word!(b"is-greater-than-variable-", variable)
137            }
138            Assertion::IsGreaterThanOrEqualVariable(variable) => {
139                concat_word!(b"is-greater-than-or-equal-variable-", variable)
140            }
141            Assertion::NonEmptyCountable(negatable) => {
142                if *negatable {
143                    word("non-empty-countable")
144                } else {
145                    word("=non-empty-countable")
146                }
147            }
148        }
149    }
150
151    #[must_use]
152    pub fn to_hash(&self) -> u64 {
153        FixedState::default().hash_one(self.to_atom())
154    }
155
156    #[must_use]
157    pub fn is_negation(&self) -> bool {
158        matches!(
159            self,
160            Assertion::Falsy
161                | Assertion::IsNotType(_)
162                | Assertion::IsNotEqual(_)
163                | Assertion::IsNotIdentical(_)
164                | Assertion::IsNotIsset
165                | Assertion::NotInArray(..)
166                | Assertion::ArrayKeyDoesNotExist
167                | Assertion::DoesNotHaveArrayKey(_)
168                | Assertion::DoesNotHaveExactCount(_)
169                | Assertion::DoesNotHaveNonnullEntryForKey(_)
170                | Assertion::DoesNotHasAtLeastCount(_)
171                | Assertion::EmptyCountable
172                | Assertion::Empty
173                | Assertion::NotCountable(_)
174        )
175    }
176
177    #[must_use]
178    pub fn has_isset(&self) -> bool {
179        matches!(
180            self,
181            Assertion::IsIsset | Assertion::ArrayKeyExists | Assertion::HasStringArrayAccess | Assertion::IsEqualIsset
182        )
183    }
184
185    #[must_use]
186    pub fn has_equality(&self) -> bool {
187        matches!(
188            self,
189            Assertion::InArray(_)
190                | Assertion::HasIntOrStringArrayAccess
191                | Assertion::HasStringArrayAccess
192                | Assertion::IsEqualIsset
193                | Assertion::IsIdentical(_)
194                | Assertion::IsNotIdentical(_)
195                | Assertion::IsEqual(_)
196                | Assertion::IsNotEqual(_)
197                | Assertion::HasExactCount(_)
198        )
199    }
200
201    #[must_use]
202    pub fn has_literal_value(&self) -> bool {
203        self.get_type().is_some_and(|atomic| {
204            atomic.is_literal_int()
205                || atomic.is_literal_float()
206                || atomic.is_known_literal_string()
207                || atomic.is_literal_class_string()
208        })
209    }
210
211    #[must_use]
212    pub fn with_type(&self, atomic: TAtomic) -> Self {
213        match self {
214            Assertion::IsType(_) => Assertion::IsType(atomic),
215            Assertion::IsNotType(_) => Assertion::IsNotType(atomic),
216            Assertion::IsIdentical(_) => Assertion::IsIdentical(atomic),
217            Assertion::IsNotIdentical(_) => Assertion::IsNotIdentical(atomic),
218            Assertion::IsEqual(_) => Assertion::IsEqual(atomic),
219            Assertion::IsNotEqual(_) => Assertion::IsNotEqual(atomic),
220            _ => self.clone(),
221        }
222    }
223
224    #[must_use]
225    pub fn get_type(&self) -> Option<&TAtomic> {
226        match self {
227            Assertion::IsIdentical(atomic)
228            | Assertion::IsNotIdentical(atomic)
229            | Assertion::IsType(atomic)
230            | Assertion::IsNotType(atomic)
231            | Assertion::IsEqual(atomic)
232            | Assertion::IsNotEqual(atomic) => Some(atomic),
233            _ => None,
234        }
235    }
236
237    pub fn get_type_mut(&mut self) -> Option<&mut TAtomic> {
238        match self {
239            Assertion::IsIdentical(atomic)
240            | Assertion::IsNotIdentical(atomic)
241            | Assertion::IsType(atomic)
242            | Assertion::IsNotType(atomic)
243            | Assertion::IsEqual(atomic)
244            | Assertion::IsNotEqual(atomic) => Some(atomic),
245            _ => None,
246        }
247    }
248
249    #[must_use]
250    pub fn resolve_templates(&self, codebase: &CodebaseMetadata, template_result: &TemplateResult) -> Vec<Self> {
251        match self {
252            Assertion::IsType(atomic) => {
253                let union = TUnion::from_single(Cow::Owned(atomic.clone()));
254                let resolved_union = inferred_type_replacer::replace(&union, template_result, codebase);
255
256                let mut result = vec![];
257                for resolved_atomic in resolved_union.types.into_owned() {
258                    result.push(Assertion::IsType(resolved_atomic));
259                }
260
261                if result.is_empty() {
262                    result.push(Assertion::IsType(TAtomic::Never));
263                }
264
265                result
266            }
267            Assertion::IsNotType(atomic) => {
268                let union = TUnion::from_single(Cow::Owned(atomic.clone()));
269                let resolved_union = inferred_type_replacer::replace(&union, template_result, codebase);
270
271                let mut result = vec![];
272                for resolved_atomic in resolved_union.types.into_owned() {
273                    result.push(Assertion::IsNotType(resolved_atomic));
274                }
275
276                if result.is_empty() {
277                    result.push(Assertion::IsNotType(TAtomic::Never));
278                }
279
280                result
281            }
282            Assertion::InArray(union) => {
283                let resolved_union = inferred_type_replacer::replace(union, template_result, codebase);
284
285                vec![Assertion::InArray(resolved_union)]
286            }
287            Assertion::NotInArray(union) => {
288                let resolved_union = inferred_type_replacer::replace(union, template_result, codebase);
289
290                vec![Assertion::NotInArray(resolved_union)]
291            }
292            _ => {
293                vec![self.clone()]
294            }
295        }
296    }
297
298    #[must_use]
299    pub fn is_negation_of(&self, other: &Assertion) -> bool {
300        match self {
301            Assertion::Any => false,
302            Assertion::Falsy => matches!(other, Assertion::Truthy),
303            Assertion::Truthy => matches!(other, Assertion::Falsy),
304            Assertion::IsType(atomic) => match other {
305                Assertion::IsNotType(other_atomic) => other_atomic == atomic,
306                _ => false,
307            },
308            Assertion::IsNotType(atomic) => match other {
309                Assertion::IsType(other_atomic) => other_atomic == atomic,
310                _ => false,
311            },
312            Assertion::IsIdentical(atomic) => match other {
313                Assertion::IsNotIdentical(other_atomic) => other_atomic == atomic,
314                _ => false,
315            },
316            Assertion::IsNotIdentical(atomic) => match other {
317                Assertion::IsIdentical(other_atomic) => other_atomic == atomic,
318                _ => false,
319            },
320            Assertion::IsEqual(atomic) => match other {
321                Assertion::IsNotEqual(other_atomic) => other_atomic == atomic,
322                _ => false,
323            },
324            Assertion::IsNotEqual(atomic) => match other {
325                Assertion::IsEqual(other_atomic) => other_atomic == atomic,
326                _ => false,
327            },
328            Assertion::IsEqualIsset => false,
329            Assertion::IsIsset => matches!(other, Assertion::IsNotIsset),
330            Assertion::IsNotIsset => matches!(other, Assertion::IsIsset),
331            Assertion::HasStringArrayAccess => false,
332            Assertion::HasIntOrStringArrayAccess => false,
333            Assertion::ArrayKeyExists => matches!(other, Assertion::ArrayKeyDoesNotExist),
334            Assertion::ArrayKeyDoesNotExist => matches!(other, Assertion::ArrayKeyExists),
335            Assertion::HasArrayKey(str) => match other {
336                Assertion::DoesNotHaveArrayKey(other_str) => other_str == str,
337                _ => false,
338            },
339            Assertion::DoesNotHaveArrayKey(str) => match other {
340                Assertion::HasArrayKey(other_str) => other_str == str,
341                _ => false,
342            },
343            Assertion::HasNonnullEntryForKey(str) => match other {
344                Assertion::DoesNotHaveNonnullEntryForKey(other_str) => other_str == str,
345                _ => false,
346            },
347            Assertion::DoesNotHaveNonnullEntryForKey(str) => match other {
348                Assertion::HasNonnullEntryForKey(other_str) => other_str == str,
349                _ => false,
350            },
351            Assertion::InArray(union) => match other {
352                Assertion::NotInArray(other_union) => other_union == union,
353                _ => false,
354            },
355            Assertion::NotInArray(union) => match other {
356                Assertion::InArray(other_union) => other_union == union,
357                _ => false,
358            },
359            Assertion::Empty => matches!(other, Assertion::NonEmpty),
360            Assertion::NonEmpty => matches!(other, Assertion::Empty),
361            Assertion::NonEmptyCountable(negatable) => {
362                if *negatable {
363                    matches!(other, Assertion::EmptyCountable)
364                } else {
365                    false
366                }
367            }
368            Assertion::EmptyCountable => matches!(other, Assertion::NonEmptyCountable(true)),
369            Assertion::HasExactCount(number) => match other {
370                Assertion::DoesNotHaveExactCount(other_number) => other_number == number,
371                _ => false,
372            },
373            Assertion::DoesNotHaveExactCount(number) => match other {
374                Assertion::HasExactCount(other_number) => other_number == number,
375                _ => false,
376            },
377            Assertion::HasAtLeastCount(number) => match other {
378                Assertion::DoesNotHasAtLeastCount(other_number) => other_number == number,
379                _ => false,
380            },
381            Assertion::DoesNotHasAtLeastCount(number) => match other {
382                Assertion::HasAtLeastCount(other_number) => other_number == number,
383                _ => false,
384            },
385            Assertion::IsLessThan(number) => match other {
386                Assertion::IsGreaterThanOrEqual(other_number) => other_number == number,
387                _ => false,
388            },
389            Assertion::IsLessThanOrEqual(number) => match other {
390                Assertion::IsGreaterThan(other_number) => other_number == number,
391                _ => false,
392            },
393            Assertion::IsGreaterThan(number) => match other {
394                Assertion::IsLessThanOrEqual(other_number) => other_number == number,
395                _ => false,
396            },
397            Assertion::IsGreaterThanOrEqual(number) => match other {
398                Assertion::IsLessThan(other_number) => other_number == number,
399                _ => false,
400            },
401            Assertion::IsLessThanFromBound(_)
402            | Assertion::IsLessThanOrEqualFromBound(_)
403            | Assertion::IsGreaterThanFromBound(_)
404            | Assertion::IsGreaterThanOrEqualFromBound(_) => false,
405            Assertion::IsLessThanVariable(variable) => match other {
406                Assertion::IsGreaterThanOrEqualVariable(other_variable) => other_variable == variable,
407                _ => false,
408            },
409            Assertion::IsLessThanOrEqualVariable(variable) => match other {
410                Assertion::IsGreaterThanVariable(other_variable) => other_variable == variable,
411                _ => false,
412            },
413            Assertion::IsGreaterThanVariable(variable) => match other {
414                Assertion::IsLessThanOrEqualVariable(other_variable) => other_variable == variable,
415                _ => false,
416            },
417            Assertion::IsGreaterThanOrEqualVariable(variable) => match other {
418                Assertion::IsLessThanVariable(other_variable) => other_variable == variable,
419                _ => false,
420            },
421            Assertion::Countable => matches!(other, Assertion::NotCountable(negatable) if *negatable),
422            Assertion::NotCountable(_) => matches!(other, Assertion::Countable),
423        }
424    }
425
426    #[must_use]
427    pub fn get_negation(&self) -> Self {
428        match self {
429            Assertion::Any => Assertion::Any,
430            Assertion::Falsy => Assertion::Truthy,
431            Assertion::IsType(atomic) => Assertion::IsNotType(atomic.clone()),
432            Assertion::IsNotType(atomic) => Assertion::IsType(atomic.clone()),
433            Assertion::Truthy => Assertion::Falsy,
434            Assertion::IsIdentical(atomic) => Assertion::IsNotIdentical(atomic.clone()),
435            Assertion::IsNotIdentical(atomic) => Assertion::IsIdentical(atomic.clone()),
436            Assertion::IsEqual(atomic) => Assertion::IsNotEqual(atomic.clone()),
437            Assertion::IsNotEqual(atomic) => Assertion::IsEqual(atomic.clone()),
438            Assertion::IsIsset => Assertion::IsNotIsset,
439            Assertion::IsNotIsset => Assertion::IsIsset,
440            Assertion::Empty => Assertion::NonEmpty,
441            Assertion::NonEmpty => Assertion::Empty,
442            Assertion::NonEmptyCountable(negatable) => {
443                if *negatable {
444                    Assertion::EmptyCountable
445                } else {
446                    Assertion::Any
447                }
448            }
449            Assertion::EmptyCountable => Assertion::NonEmptyCountable(true),
450            Assertion::ArrayKeyExists => Assertion::ArrayKeyDoesNotExist,
451            Assertion::ArrayKeyDoesNotExist => Assertion::ArrayKeyExists,
452            Assertion::InArray(union) => Assertion::NotInArray(union.clone()),
453            Assertion::NotInArray(union) => Assertion::InArray(union.clone()),
454            Assertion::HasExactCount(size) => Assertion::DoesNotHaveExactCount(*size),
455            Assertion::DoesNotHaveExactCount(size) => Assertion::HasExactCount(*size),
456            Assertion::HasAtLeastCount(size) => Assertion::DoesNotHasAtLeastCount(*size),
457            Assertion::DoesNotHasAtLeastCount(size) => Assertion::HasAtLeastCount(*size),
458            Assertion::HasArrayKey(str) => Assertion::DoesNotHaveArrayKey(*str),
459            Assertion::DoesNotHaveArrayKey(str) => Assertion::HasArrayKey(*str),
460            Assertion::HasNonnullEntryForKey(str) => Assertion::DoesNotHaveNonnullEntryForKey(*str),
461            Assertion::DoesNotHaveNonnullEntryForKey(str) => Assertion::HasNonnullEntryForKey(*str),
462            Assertion::HasStringArrayAccess => Assertion::Any,
463            Assertion::HasIntOrStringArrayAccess => Assertion::Any,
464            Assertion::IsEqualIsset => Assertion::Any,
465            Assertion::IsLessThan(number) => Assertion::IsGreaterThanOrEqual(*number),
466            Assertion::IsLessThanOrEqual(number) => Assertion::IsGreaterThan(*number),
467            Assertion::IsGreaterThan(number) => Assertion::IsLessThanOrEqual(*number),
468            Assertion::IsGreaterThanOrEqual(number) => Assertion::IsLessThan(*number),
469            Assertion::IsLessThanFromBound(_)
470            | Assertion::IsLessThanOrEqualFromBound(_)
471            | Assertion::IsGreaterThanFromBound(_)
472            | Assertion::IsGreaterThanOrEqualFromBound(_) => Assertion::Any,
473            Assertion::IsLessThanVariable(variable) => Assertion::IsGreaterThanOrEqualVariable(*variable),
474            Assertion::IsLessThanOrEqualVariable(variable) => Assertion::IsGreaterThanVariable(*variable),
475            Assertion::IsGreaterThanVariable(variable) => Assertion::IsLessThanOrEqualVariable(*variable),
476            Assertion::IsGreaterThanOrEqualVariable(variable) => Assertion::IsLessThanVariable(*variable),
477            Assertion::Countable => Assertion::NotCountable(true),
478            Assertion::NotCountable(_) => Assertion::Countable,
479        }
480    }
481
482    /// Whether this assertion represents the condition itself and can be used
483    /// while constructing the opposite branch.
484    ///
485    /// Facts inferred from a non-literal range bound are only consequences of
486    /// a condition. They are useful in the true branch but are not logically
487    /// equivalent to that condition, so negating them would be unsound.
488    #[inline]
489    #[must_use]
490    pub const fn is_negatable(&self) -> bool {
491        !matches!(
492            self,
493            Self::IsLessThanFromBound(_)
494                | Self::IsLessThanOrEqualFromBound(_)
495                | Self::IsGreaterThanFromBound(_)
496                | Self::IsGreaterThanOrEqualFromBound(_)
497        )
498    }
499
500    /// Returns the expression referenced by a relational assertion.
501    #[inline]
502    #[must_use]
503    pub const fn referenced_variable(&self) -> Option<Word> {
504        match self {
505            Self::IsLessThanVariable(variable)
506            | Self::IsLessThanOrEqualVariable(variable)
507            | Self::IsGreaterThanVariable(variable)
508            | Self::IsGreaterThanOrEqualVariable(variable) => Some(*variable),
509            _ => None,
510        }
511    }
512}