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